前端请求超过 3 秒,怎么分析原因?

1.看前端和网络

F12开发者模式去查看network,首先判断是前端问题还是后端问题

通过查看接口 Waiting 时间进行判断是后端响应时间太长还是说前端渲染问题

2.给后端接口添加日志进一步定位后端问题

3.如果是数据库问题,则可能是慢查询问题

  1. 查看慢查询日志,定位是哪个查询语句出问题了
  2. explain对应的sql语句,看看对应的索引和索引利用情况
  3. 如果是索引方面是问题,看看是不是索引缺失或者失效的问题

4.看 Redis 问题

常见原因:

大 key、连接池耗尽。

5.看外部接口或第三方服务

比如短信、支付、AI 模型、地图接口

问题经常是:

没有设置超时时间。

如果第三方接口卡住,后端线程也会一直阻塞。

解决方案:

第三方接口超过 1 秒直接失败,返回默认结果,避免拖垮整个接口。

6.看线程池和连接池

如果并发高,请求超过 3 秒可能不是单个逻辑慢,而是排队。

比如:

Tomcat 线程池满了,请求进来后没有线程处理,只能排队。
数据库连接池满了,业务线程拿不到连接,也会等待。

Redis 不加限制会耗尽内存,怎么解决?

Redis 是内存数据库,如果不限制内存,业务一直写 key,最终可能导致内存被打满,轻则 Redis 变慢,重则被操作系统 OOM Kill。所以生产环境一定要做内存限制和淘汰策略。

  1. 设置最大内存 maxmemory
  2. 设置淘汰策略 maxmemory-policy
  3. 给缓存 key 设置过期时间
  4. 过期时间加随机值,防止缓存雪崩
  5. 避免大 key

那淘汰策略有哪些呢?

  • 不淘汰任何 key,内存满后写入直接报错
  • 从所有 key 中淘汰最近最少使用的数据
  • 从所有 key 中随机淘汰
  • 优先淘汰剩余过期时间最短的 key

MyBatis 为什么 Mapper 接口不用实现?是什么机制?

MyBatis 的 Mapper 接口不用我们手动写实现类,本质原因是 MyBatis 在启动时通过 JDK 动态代理,为 Mapper 接口自动生成了代理对象。
我们调用 Mapper 方法时,实际上调用的是代理对象,而不是接口本身。

如果只是小厂的话,这样你也就够了,但是如果你面试一些大厂可能就会衍生出一些问题:

  1. 动态代理是什么?底层原理是什么?动态代理还有哪些应用场景
  2. 调用Mapper方法时候的流程是什么?

我们先回答问题2

调用Mapper方法时候的流程是什么?

  1. Spring扫描 Mapper 接口
  2. 为接口创建 MapperProxyFactory(专门生成 Mapper 代理对象)
  3. 生成代理对象(Spring 容器里放的是 proxy)
  4. 调用mapper方法的时候拦截方法
  5. 通过 SQL 唯一标识(com.xxx.mapper.UserMapper.selectById)找到对应的 MappedStatement(MyBatis 启动时会把 XML 或注解 SQL 解析成:MappedStatement)
  6. 调用 SqlSession 执行 SQL
  7. SqlSession 调用 Executor
  8. Executor 调用 JDBC

(1)获取数据库连接

(2)预编译 SQL(select * from user where id = ?)

(3)参数绑定

(4)执行 SQL

9.ResultSet 结果映射

数据库返回:ResultSet

MyBatis 会根据:resultType或者resultMap映射成 Java 对象。

那引出问题:什么是预编译?

数据库会:

1.SQL 语法解析

数据库先检查:

  • SQL 是否合法
  • 表是否存在
  • 字段是否存在

2. SQL 优化

  • 走哪个索引
  • 如何生成执行计划
  • 是否全表扫描

3.生成执行计划

数据库会缓存:执行计划

后面参数不同:可以复用

那我们是不是可以想起之前背八股文的时候,学的#{}和${}的区别是什么

#{} 是预编译参数

例如:

<select>
select * from user where id = #{id}
</select>

MyBatis 最终:

select * from user where id = ?

然后:

ps.setLong(1,1L)

支持:

  • 预编译
  • 执行计划复用
  • 防 SQL 注入

${}是字符串拼接

例如:

<select>
select * from user where id = ${id}
</select>

如果:

id=1

最终 SQL:

select * from user where id = 1

如果:

id=2

SQL 会变成:

select * from user where id = 2

每次都需要:

  • 重新解析
  • 重新优化
  • 重新生成执行计划
  • 更严重的问题:SQL 注入

这里顺带大家回顾一下第一个问题(实则是我也忘记了)

什么是动态代理?

动态代理本质上是在程序运行期间,动态生成一个代理对象,通过代理对象去增强目标对象的方法,而不需要手动编写代理类。

动态代理底层原理是什么?

Java 动态代理主要有两种:

类型 使用场景
JDK 动态代理 代理接口
CGLIB 动态代理 代理类

JDK 动态代理

目标对象实现接口

CGLIB 动态代理

有些类没有接口

底层流程

(1)创建代理对象(Proxy.newProxyInstance())

(2)方法被拦截

调用:

proxy.login()

实际上进入:

InvocationHandler.invoke()

动态代理有哪些应用场景?

  1. Spring AOP
  2. Spring 事务(@Transactional)
  3. MyBatis Mapper
  4. 日志监控

那基于此是不是又可以引出问题:

  1. AOP是什么?AOP的底层原理是什么?AOP的实现方法有哪些
  2. Spring的事务是什么,有哪些实现方式

AOP 是什么?AOP 底层原理是什么?

AOP 全称是面向切面编程,它的核心思想是在不修改原业务代码的情况下,对方法进行统一增强。比如日志记录、权限校验、性能统计、事务管理这些公共逻辑,如果直接写在业务代码里会产生大量重复代码,而 AOP 可以把这些逻辑抽离成切面,统一织入到目标方法中。


Spring AOP 的底层核心其实是“动态代理 + 反射”。Spring 不会直接把原对象交给我们,而是会在运行期间为目标对象生成一个代理对象。我们调用方法时,实际上先进入的是代理对象,而不是原对象。代理对象会先执行增强逻辑,比如打印日志、开启事务等,然后 JVM 会把当前调用的方法封装成一个 Method 对象传给代理逻辑,例如调用 proxy.login() 时,JVM 会自动把 login 方法对应的 Method 传进来。接着代理对象会通过反射 method.invoke(target,args) 去调用真实业务对象的方法,最后再执行方法后的增强逻辑,比如提交事务、统计耗时等。所以可以理解为:AOP 的核心目的是增强方法;动态代理负责生成代理对象并拦截方法;反射负责真正调用目标对象的方法。

(proxy, method, args) -> {

    // AOP增强
    System.out.println("before");

    // 反射调用真实方法
    Object result =
        method.invoke(target,args);

    // AOP增强
    System.out.println("after");

    return result;
}

反射是什么?底层原理是什么?

反射本质上是 Java 在运行时动态获取类信息,并动态操作类的一种机制。正常情况下,我们是“编译时”就确定调用哪个类、哪个方法,比如 userService.login();而反射可以在程序运行过程中,动态获取类、方法、属性的信息,并动态创建对象、调用方法。比如可以通过 Class 获取类信息,通过 Method 获取方法,再通过 method.invoke() 动态执行方法。

AOP的实现方法有哪些

1.基于 XML 配置实现

2.基于注解实现,也是目前最常用的方式。通常会使用 @Aspect 定义切面类,使用 @Before@After@Around 等定义通知,再通过切点表达式指定拦截哪些方法。例如日志、权限、事务等功能通常都是这样实现的。

3.基于自定义注解实现

Spring的事务是什么,有哪些实现方式

Spring 事务本质上是 Spring 对数据库事务的一层封装,用来保证一组数据库操作要么全部成功,要么全部失败,从而保证数据一致性。比如转账场景,A 扣钱和 B 加钱必须同时成功,如果其中一步失败,就需要整体回滚,否则数据就会出现问题。Spring 事务底层核心其实是 AOP,Spring 会通过动态代理为目标对象生成事务代理,在方法执行前开启事务,执行成功后提交事务,发生异常时回滚事务。

Spring 事务主要有两种实现方式:编程式事务和声明式事务

编程式事务:TransactionTemplate

声明式事务:@Transactional 注解实现

mysql索引按存储方式划分

MySQL 索引按照存储方式或者底层数据结构来划分,主要可以分为 B+Tree 索引、Hash 索引

其中 InnoDB 最核心、最常用的是 B+Tree 索引。B+Tree 属于多路平衡查找树,特点是树高度低、磁盘 IO 次数少,并且叶子节点之间通过链表连接,因此不仅适合等值查询,还非常适合范围查询、排序和分组

RAG 技术的挑战点是什么?

它最难的地方不在“调用大模型”,而在怎么把正确的知识找出来,并且让模型只基于正确知识回答

1.文档解析难

企业文档不是纯文本,可能是 Word、PDF、Excel、CSV、图片扫描件。不同格式结构不一样。
比如 Word 里有标题、正文、表格;Excel 里是行列结构;PDF 可能有分页、页眉页脚、换行错乱。解析错了,后面检索一定差。

2.chunk 切分难
chunk 太大,会把很多无关内容塞进上下文;chunk 太小,又会丢上下文

3.召回难
用户问法和文档写法不一样。

如果只靠关键词,“出差花的钱”和“差旅费用报销”不是完全一样,可能召回不到
所以要做 query rewrite、同义词、关键词召回 + 向量召回

4.排序难
召回出来的 topK 不一定最准确,所以要 rerank.

5.拒答难

资料里没有答案时要拒答,资料里有答案时要回答。这件事非常难。

6.权限隔离难

企业文档有权限,A 部门不能看到 B 部门文档。
这个不能只靠 Prompt,必须在检索层做 filter。

那么如何去解决这些挑战

1. 文档解析难

首先按文件类型走不同解析器:

Word → 解析标题、段落、表格
PDF → 解析页码、段落、表格,处理页眉页脚
Excel/CSV → 按 sheet、行、列解析,保留表头和字段关系
图片扫描件 → OCR 识别文字

其次,解析结果不要只保存纯文本,还要保留结构信息

例如word文档

标题:3.2 报销审批规则

报销金额小于1000元,由直属主管审批。

报销金额大于等于1000元,由部门负责人审批。

{
  "title": "员工报销制度",
  "section": "3.2 报销审批规则",
  "level": 2,
  "content": "报销金额大于等于1000元,由部门负责人审批。",
  "page": 5
}

2. chunk 切分难

不能只按固定长度切,要做结构化切分。

可以按:

标题
章节
段落
自然语义边界

例如制度文档:

3.2 报销审批规则
报销金额小于1000元,由直属主管审批。
报销金额大于等于1000元,由部门负责人审批。

切 chunk 时要保留标题:

标题:3.2 报销审批规则
内容:报销金额大于等于1000元,由部门负责人审批。

还可以加 overlap,避免上下文断裂

比如 chunk1 末尾和 chunk2 开头重叠一部分:

chunk1: ...小于1000元由主管审批。大于等于1000元...
chunk2: 大于等于1000元由部门负责人审批...

这里可以给每个chuck段添加一些额外信息,比如属于哪个文档,文档中的第几个chuck,后面可以进行相邻chuck的merge

3. 召回难

1.做 Query Rewrite

把用户问题改写成更适合检索的形式:

第一层是固定处理规则

去标点
统一大小写
去掉口语填充词
空格归一化
章节编号归一化
中英文数字/符号归一化

2.使用混合召回

使用关键词+向量召回

关键词 topK + 向量 topK → 融合去重 → 进入 rerank

4. 排序难

做 rerank 重排。重排时不能只看向量分数,要结合多个信号:

关键词命中
标题命中
实体命中
字段命中
结构化命中
chunk 位置
文档权重
内容长度惩罚

5. 拒答难

难点是避免两种问题:

没答案却乱答 → 幻觉
有答案却说不知道 → 误拒答

Answer Guard,根据多个信号判断:

top1 分数
top1 和 top2 分差
关键词命中率
实体是否命中
字段是否命中
上下文覆盖度
结构化直接命中

6. 权限隔离难

第一,文档入库时打权限标签。

每个文档、每个 chunk 都带权限字段:

{
  "documentId": 1001,
  "departmentId": "B",
  "acl": ["dept:B", "role:manager"]
}

第二,检索时强制加 filter。

A 部门用户查询时,只能召回:

dept:A
public

不能召回 B 部门文档。

ES 查询里加:

{
  "terms": {
    "acl": ["dept:A", "public"]
  }
}

第三,rerank/上下文构建阶段 前二次校验

第四,答案必须引用来源。

ES 里存什么?为什么用 ES,而不是 Milvus 做向量数据库?

ES 存的不是“整个文档”,而是“chunk 级别的检索索引”。

ES 底层最核心的数据结构其实是“倒排索引

ES 底层最核心的数据结构是倒排索引(Inverted Index)。传统数据库更像是“文档到内容”的正向存储,而 ES 会建立“词项到文档”的映射关系。

例如文档里有“李四分数88”,ES 在写入时会先经过分词器,把内容拆成“李四、分数、88”等 term,然后建立:

李四 → doc1
分数 → doc1
88 → doc1

这样用户搜索“李四”时,就不需要全表扫描,而是直接通过倒排索引快速定位文档,所以检索效率很高。

倒排索引里除了 term 和 docId,还会保存 term frequency、position、offset 等信息,用于 BM25 相关性计算、短语匹配和高亮显示。

在我这个 RAG 项目里,ES 不仅存倒排索引,还存 embedding 向量,所以本质上是:

倒排索引 + 向量索引

的混合检索架构。

倒排索引负责关键词、人名、编号、字段等精确检索;向量索引负责语义相似召回;最后再结合 rerank 做融合排序。


Milvus 是专业向量数据库,确实很适合大规模向量检索。
但你的项目不是“纯向量检索系统”,而是“企业 RAG 问答系统”。

企业 RAG 需要的不只是向量,还需要:

关键词检索
权限过滤
结构化字段
文档状态
调试解释
多条件过滤

第一,ES 能做混合检索

BM25 关键词召回
dense_vector 向量召回
filter 权限过滤
字段查询

第二,ES 过滤能力更方便。

比如 A 部门用户检索时,可以直接加:

{
  "term": {
    "departmentId": "A"
  }
}

第三,ES 对调试更友好。

你可以看:

命中了哪些关键词
score 是多少
哪个字段命中
召回了哪个 chunk
为什么排在前面

Embedding 模型是什么维度?为什么不用网格划分?

Embedding 维度取决于你用的模型。
比如有的模型输出 768 维,有的 1024 维,有的 1536 维。
ES mapping 里的 dense_vector 维度必须和 embedding 模型输出维度一致。

比如模型输出 1024 维,ES 里就要建:

{
  "contentVector": {
    "type": "dense_vector",
    "dims": 1024
  }
}

“网格划分”适合低维空间,比如地图经纬度。

比如二维地图:

x轴:经度
y轴:纬度

可以切成很多小格子:

A1 A2 A3
B1 B2 B3
C1 C2 C3

查附近的人时,只查附近几个格子。

但 embedding 是高维向量,比如 768 维、1024 维。

如果维度降维可不可以?

可以,但不能随便降。
降维的本质是:

把高维向量压缩成低维向量

比如:

1024维 → 256维
1536维 → 384维

好处:

存储减少
索引更小
检索更快
内存占用更低

坏处:

语义信息损失
召回准确率可能下降
相似度分布改变

最近用 AI 做开发的具体需求,如何思考设计并落地?

可以讲你做 Conversation Memory

原问题:

RAG 问答一开始是单轮的。

用户第一轮问:


张三在哪个部门?

系统回答:


张三在技术部。

第二轮用户问:

那他的负责人是谁?

系统不知道“他”是谁,也不知道上一轮的上下文。

所以你要加会话记忆。

设计时你考虑了几个点。

第一,必须有 conversationId。

不然不同用户、不同会话会串。

请求里加:

{
  "question": "那他的负责人是谁?",
  "conversationId": "conv_123"
}

第二,不做复杂长期记忆,只做 Redis 窗口记忆。

比如只存最近 5 轮:

user: 张三在哪个部门?
assistant: 张三在技术部。
user: 那他的负责人是谁?

第三,历史只参与生成,不参与检索。

这点很重要。

如果把历史全部拼进检索 query,可能会导致召回偏移。
所以 保守设计:

retrieval 仍然只基于当前 question
history 只放进 prompt 里帮助生成

第四,Prompt 里规定优先级:

当前参考知识 > 历史对话

因为历史可能过期,当前检索到的知识更可靠。

第五,缓存要改。

如果缓存 key 只看 question,那么:

那他的负责人是谁?

这个问题在不同会话里会有不同含义。
所以缓存 key 要包含:

conversationId
question
history signature

落地步骤:

QaAskRequest 增加 conversationId
QaAskResponse 增加 conversationId、memoryUsed、historyCount
新增 RedisChatMemoryRepository
新增 ConversationMemorySupport
ask 前读取历史
ask 后写入 user/assistant
PromptBuilder 加入历史对话
缓存 key 加入 conversationId 和历史签名

你用 AI 的方式可以这样讲:

你不是让 AI 直接瞎写代码,而是:

先让 AI 帮我拆设计方案
→ 我确定 V1 边界:只做窗口记忆,不做复杂指代消解
→ 让 AI 生成类和接口修改草案
→ 我审查缓存 key、Redis 结构、Prompt 优先级
→ 最后用多轮问题联调验证

多表聚合查询:用户表、订单表、支付表

题目:

查询 2026 年注册,且在注册后 30 天内成功支付金额大于 1000 的用户 ID
SELECT DISTINCT u.id
FROM user u
JOIN orders o ON o.user_id = u.id
JOIN payment p ON p.order_id = o.id
WHERE u.register_time >= '2026-01-01'
  AND u.register_time < '2027-01-01'
  AND p.pay_status = 'SUCCESS'
  AND p.pay_time >= u.register_time
  AND p.pay_time < DATE_ADD(u.register_time, INTERVAL 30 DAY)
  AND p.pay_amount > 1000;

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐