2025-2026 互联网中大厂高频面试八股 TOP80(含AI工程化专题)
来源 :综合字节、阿里、腾讯、美团、京东、百度、快手、滴滴、小红书、米哈游等中大厂 2025--2026 年真实面经与高频题库
定位 :后端开发 + AI 应用开发工程师(大模型应用开发方向)
说明:标 ⭐ 为几乎每场必问;标 🔥 为出现率极高;AI 工程化部分为 2026 年新增高频方向
一、Java 基础 & 集合(8 道)
1. ⭐ HashMap 底层原理,JDK 1.7 与 1.8 的区别?为什么引入红黑树?扩容机制?线程安全问题?
答案:
HashMap 底层基于数组 + 链表 + 红黑树实现。JDK 1.7 是纯数组+链表,JDK 1.8 当链表长度≥8 且数组长度≥64 时转为红黑树,链表长度≤6 时退化为链表。
引入红黑树的原因:纯链表在哈希冲突严重时查询退化为 O(n),红黑树保证 O(log n)。但树节点内存占用是普通节点的 2 倍,且维护成本高,所以设置阈值 8(泊松分布统计,冲突达到 8 的概率极低,仅 0.00000606)。
扩容机制 :默认初始容量 16,负载因子 0.75。当 size > threshold = capacity * loadFactor 时触发扩容,扩容为原来的 2 倍。JDK 1.7 采用头插法迁移数据,并发下会产生死循环(环形链表);JDK 1.8 改为尾插法,虽不会死循环,但仍存在数据丢失问题,故非线程安全。
线程安全替代方案 :ConcurrentHashMap(推荐)、Collections.synchronizedMap。
2. ⭐ ConcurrentHashMap 如何保证线程安全?JDK 1.7 与 1.8 的区别?
答案:
JDK 1.7 :采用分段锁(Segment) ,将数组划分为 16 段(默认),每段继承 ReentrantLock。线程访问某段数据时只锁定该段,其他段不受影响,实现细粒度并发控制。
JDK 1.8 :取消分段锁,采用 CAS + synchronized。结构变为数组+链表+红黑树。插入时:
- 若桶位为空,使用 CAS 直接插入;
- 若存在冲突,则对该桶位头节点加
synchronized(锁粒度为单个桶位,远低于 1.7 的段锁)。
其他优化 :1.8 引入 sizeCtl、transferIndex 等变量,扩容时支持多线程协助迁移 (transfer 方法),提升扩容效率。
3. 🔥 ArrayList 与 LinkedList 底层实现、扩容机制、适用场景?
答案:
ArrayList :底层是 Object[] 数组。默认初始容量 10,扩容时增长为原来的 1.5 倍 (oldCapacity + (oldCapacity >> 1)),使用 Arrays.copyOf 复制数据。支持随机访问,查询 O(1),增删中间元素 O(n)。适合查询多、增删少的场景。
LinkedList :底层是双向链表 (Node<E> 包含 prev、item、next)。无需扩容,增删只需修改指针 O(1),但查询需遍历 O(n)。适合频繁增删、队列/栈实现的场景。
工程注意:
ArrayList若已知数据量,应通过构造函数指定初始容量,避免多次扩容带来的数组拷贝开销。LinkedList内存占用更高(每个节点需维护两个指针),且缓存局部性差,现代 CPU 下遍历性能往往不如ArrayList。
4. ⭐ synchronized 底层原理,锁升级过程?
答案:
synchronized 底层通过 Monitor(管程) 实现,依赖对象头的 Mark Word 存储锁状态。
锁升级过程(不可逆,除偏向锁可撤销):
- 无锁:对象刚创建,无竞争。
- 偏向锁 :单线程反复进入同步块,Mark Word 记录线程 ID。下次该线程进入只需 CAS 替换线程 ID,无需原子操作。JDK 15 后默认关闭(
-XX:-UseBiasedLocking),因多核环境下竞争频繁,偏向锁撤销成本高。 - 轻量级锁 :出现竞争时,线程在栈帧创建 Lock Record,通过 CAS 将对象头指向 Lock Record。自旋等待(默认 10 次,自适应自旋根据历史调整)。
- 重量级锁 :自旋失败,线程阻塞,进入 Monitor 的
_WaitSet或_EntryList,由操作系统调度,涉及用户态/内核态切换,开销最大。
锁优化:锁消除(逃逸分析)、锁粗化(扩大同步范围减少加锁次数)。
5. ⭐ volatile 的作用?如何保证可见性和禁止指令重排序?底层原理?
答案:
volatile 保证可见性 和有序性,但不保证原子性。
可见性原理:
- 写
volatile变量时,JVM 插入 StoreStore + StoreLoad 内存屏障,将工作内存变量值刷新回主内存。 - 读
volatile变量时,插入 LoadLoad + LoadStore 内存屏障,使线程本地缓存失效,强制从主内存读取。 - 底层依赖 CPU 的 MESI 缓存一致性协议(修改/独占/共享/失效)。
禁止指令重排序:
- 编译器和 CPU 可能对指令重排优化。
volatile通过内存屏障限制重排序规则:- 写操作前的代码不能重排到写之后;
- 读操作后的代码不能重排到读之前。
典型应用 :单例模式的双重检查锁定(DCL)中,instance 必须用 volatile 修饰,防止 new 对象的指令重排序(分配内存→初始化→引用赋值 被重排为 分配内存→引用赋值→初始化)。
6. 🔥 equals() 与 hashCode() 的约定?为什么重写 equals 必须重写 hashCode?
答案:
Java 约定:
- 若两个对象
equals为true,则hashCode必须相等; - 若
hashCode相等,equals不一定为true(哈希冲突)。
为什么必须同时重写:
HashMap判断键是否存在时,先比较hashCode,若相等再比较equals。- 若只重写
equals,两个逻辑相等的对象可能因hashCode不同被放到不同桶位,导致HashMap.put出现重复键,HashMap.get返回null。
最佳实践 :使用 Objects.hash(field1, field2) 或 IDE 自动生成,保证一致性。不可变对象的 hashCode 可缓存以提高性能。
7. 🔥 Java 异常体系?Error 与 Exception 区别?受检异常与非受检异常?
答案:
Java 异常顶层是 Throwable,分为 Error 和 Exception:
- Error :严重系统级错误(如
OutOfMemoryError、StackOverflowError),程序无法恢复,不应捕获。 - Exception :程序可处理的异常,分为:
- 受检异常(Checked Exception) :编译期强制处理(如
IOException、SQLException),需try-catch或throws。 - 非受检异常(Unchecked Exception) :运行时异常(
RuntimeException及其子类,如NullPointerException、IllegalArgumentException),编译期不强制处理。
- 受检异常(Checked Exception) :编译期强制处理(如
工程实践:
- 业务异常应继承
RuntimeException,避免方法签名污染; - 捕获异常应精确,避免裸
catch (Exception e); - 异常信息应包含上下文(参数、业务单号),但生产环境日志中不得打印敏感信息。
8. 🔥 String、StringBuilder、StringBuffer 的区别?String 为什么不可变?
答案:
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(只读) | 不安全 | 安全(synchronized) |
| 性能 | 低(频繁拼接产生大量对象) | 高 | 中(同步开销) |
| 适用场景 | 字符串常量 | 单线程拼接 | 多线程拼接 |
String 不可变的原因:
- 字符串常量池复用:相同字符串共享引用,节省堆内存;若可变,常量池引用会被意外修改。
- HashCode 缓存 :
String的hashCode可缓存,作为HashMap键时性能极高。 - 线程安全:天然线程安全,无需同步。
- 安全 :网络连接、文件路径等以
String传递,不可变防止被篡改。
工程注意 :循环体内字符串拼接必须使用 StringBuilder,否则每次 + 操作都会产生新的 String 对象,导致 O(n²) 时间和大量 GC。
二、Java 并发编程(9 道)
9. ⭐ ReentrantLock 与 synchronized 的区别?AQS 底层原理?
答案:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层 | JVM 层(Monitor) | API 层(JDK) |
| 锁获取 | 自动(进入/退出代码块) | 手动 lock()/unlock() |
| 可中断 | ❌ | ✅ lockInterruptibly() |
| 超时获取 | ❌ | ✅ tryLock(timeout, unit) |
| 公平锁 | ❌ 非公平 | ✅ 可配置 |
| 条件变量 | 一个(wait/notify) |
多个 Condition |
| 性能 | JDK 6+ 优化后接近 | 略高(CAS + 自旋) |
AQS(AbstractQueuedSynchronizer)原理:
- AQS 是 JUC 包的基石,维护一个
volatile int state和一个 FIFO 双向队列(CLH 变体)。 - 获取锁:尝试 CAS 修改
state,失败则入队,线程自旋或阻塞(LockSupport.park)。 - 释放锁:修改
state,唤醒后继节点(unparkSuccessor)。 ReentrantLock中,state=0表示未锁定,state>0表示重入次数。
10. ⭐ 线程池 7 大核心参数?如何合理设置?拒绝策略?
答案:
7 大参数:
corePoolSize:核心线程数,即使空闲也保留(除非allowCoreThreadTimeOut)。maximumPoolSize:最大线程数。keepAliveTime:非核心线程空闲存活时间。unit:时间单位。workQueue:任务等待队列(ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue)。threadFactory:线程工厂,可自定义线程名、守护状态。handler:拒绝策略。
线程池大小设置:
- CPU 密集型 :
corePoolSize = CPU 核心数 + 1(+1 防止页缺失导致 CPU 空闲)。 - IO 密集型 :
corePoolSize = CPU 核心数 * 2或更大,公式:线程数 = CPU 核数 * (1 + 平均等待时间 / 平均工作时间)。
拒绝策略:
AbortPolicy(默认):抛异常;CallerRunsPolicy:由调用线程执行任务,起到流量控制作用;DiscardPolicy:静默丢弃;DiscardOldestPolicy:丢弃队列最老任务。
工程实践 :使用 ThreadPoolExecutor 手动创建,禁止 Executors 的快捷方法(FixedThreadPool 和 SingleThreadExecutor 允许无限队列,有 OOM 风险;CachedThreadPool 允许无限线程)。
11. ⭐ ThreadLocal 原理?内存泄漏问题?为什么 key 要用弱引用?
答案:
原理 :每个 Thread 对象内部维护 ThreadLocalMap(ThreadLocal.ThreadLocalMap),Map 的 Key 是 ThreadLocal 的弱引用,Value 是实际存储的对象。ThreadLocal.set() 时,数据存放到当前线程的 ThreadLocalMap 中,实现线程隔离。
内存泄漏原因:
ThreadLocalMap的 Key 是弱引用,当ThreadLocal外部强引用消失后,Key 会被 GC 回收,但 Value 是强引用,且线程存活期间ThreadLocalMap一直存在,导致 Value 无法被回收。- 尤其在线程池场景下,线程复用,
ThreadLocalMap中的脏 Entry 会持续累积。
为什么 Key 用弱引用:
- 若 Key 用强引用,即使
ThreadLocal实例不再使用,只要线程存活,Entry 就永远无法回收,必然泄漏。 - 弱引用可在
ThreadLocal无外部引用时回收 Key,但 Value 仍需手动清理。
解决方案:
- 使用完调用
threadLocal.remove(); - 使用
try-finally确保清理; - JDK 21 引入虚拟线程后,
ThreadLocal性能问题凸显,建议使用ScopedValue(JEP 446)。
12. ⭐ CAS 原理?ABA 问题如何解决?CAS 的优缺点?
答案:
CAS(Compare-And-Swap)原理:
- 底层依赖 CPU 的原子指令(如 x86 的
cmpxchg),包含三个操作数:内存位置 V、预期值 A、新值 B。 - 当且仅当 V 的值等于 A 时,才将 V 更新为 B,否则不做任何操作。整个过程是原子性的。
- Java 中通过
Unsafe类提供compareAndSwapInt等本地方法实现。
ABA 问题:
- 线程 1 读取值为 A;线程 2 将 A→B→A;线程 1 CAS 时发现仍是 A,操作成功,但实际值已被修改过。
- 在链表操作等场景下,ABA 可能导致指针指向已回收节点,引发严重错误。
解决方案:
AtomicStampedReference:增加版本号(Stamp),不仅比较值,还比较版本号。AtomicMarkableReference:增加布尔标记,用于标识节点是否被删除。
优缺点:
- 优点:无锁并发,避免线程切换开销,性能高。
- 缺点 :
- ABA 问题;
- 自旋 CAS 长时间失败会浪费 CPU(可引入退避策略);
- 只能保证单个变量原子性,无法解决多变量复合操作的原子性问题。
13. 🔥 CountDownLatch、CyclicBarrier、Semaphore 的使用场景与原理?
答案:
CountDownLatch(倒计时门闩):
- 原理:基于 AQS 的共享模式,初始化计数器
state=N,countDown()时state--,await()阻塞直到state=0。 - 场景:主线程等待多个子线程完成(如启动服务时等待多个组件初始化)。不可复用。
CyclicBarrier(循环栅栏):
- 原理:基于
ReentrantLock + Condition,线程到达屏障时计数,达到设定值时唤醒所有线程,可执行可选的Runnable任务。 - 场景:多线程分片计算,最后汇总结果(如分阶段压测)。可复用 (
reset()),支持breakBarrier处理中断。
Semaphore(信号量):
- 原理:基于 AQS 共享模式,
state表示剩余许可数,acquire()获取许可(state--),release()释放(state++)。 - 场景:限流(如数据库连接池、接口 QPS 控制)。支持公平/非公平模式。
14. ⭐ Java 内存模型(JMM)?happens-before 规则?
答案:
JMM 定义:Java 内存模型规定所有变量存储在主内存,每个线程有自己的工作内存。线程对变量的操作必须在工作内存中进行,不能直接读写主内存。
内存交互操作 :lock、unlock、read、load、use、assign、store、write。read/load 和 store/write 必须成对出现,但不保证连续执行。
happens-before 规则(无需同步即可保证有序性):
- 程序次序规则:同一个线程内,前面的操作 happens-before 后面的操作。
- 监视器锁规则 :
unlockhappens-before 后面对同一把锁的lock。 - volatile 规则 :
volatile写 happens-before 后面对该变量的读。 - 线程启动规则 :
Thread.start()happens-before 线程内所有操作。 - 线程终止规则 :线程内所有操作 happens-before 线程终止检测(
Thread.join()返回)。 - 中断规则 :
interrupt()happens-before 检测到中断事件。 - 对象终结规则 :构造函数执行 happens-before
finalize()。 - 传递性:A happens-before B,B happens-before C,则 A happens-before C。
15. 🔥 volatile + CAS 实现无锁并发(如 AtomicInteger),与锁相比的优劣?
答案:
实现原理:
AtomicInteger内部使用volatile int value保证可见性,自增操作通过Unsafe.getAndAddInt实现 CAS 循环(do-while直到成功)。- 例如
incrementAndGet:读取当前值,CAS 替换为value+1,失败则重试。
与锁的对比:
| 维度 | 无锁(volatile+CAS) | 锁(synchronized/Lock) |
|---|---|---|
| 开销 | 无线程切换,开销低 | 涉及内核态/用户态切换 |
| 粒度 | 单个变量 | 代码块/方法 |
| 公平性 | 非公平(可能饥饿) | 可配置公平锁 |
| 适用场景 | 简单计数、栈顶操作 | 复杂业务逻辑、多变量复合操作 |
| 缺点 | ABA、自旋浪费 CPU | 上下文切换开销大 |
工程选择:并发量低、竞争少时无锁更优;竞争激烈时,锁的阻塞反而比 CAS 自旋更省 CPU。
16. 🔥 死锁的四个条件?如何排查死锁?如何避免死锁?
答案:
死锁四个必要条件(Coffman 条件):
- 互斥:资源一次只能被一个线程占用;
- 占有且等待:线程持有资源同时等待其他资源;
- 不可抢占:已分配的资源不能被强制剥夺;
- 循环等待:线程间形成资源等待环路。
排查方法:
jstack -l <pid>:查看线程状态,搜索"Found one Java-level deadlock"。jconsole/VisualVM:图形化检测死锁。- 日志中关注
BLOCKED状态的线程堆栈。
避免策略:
- 破坏占有且等待 :一次性申请所有资源(如
ReentrantLock的tryLock同时获取多把锁)。 - 破坏循环等待:资源按全局顺序申请(如先申请锁 A,再申请锁 B)。
- 设置超时 :
tryLock(timeout),超时则释放已持有锁并重试。 - 使用无锁编程:CAS、StampedLock 的乐观读。
17. 🔥 CompletableFuture 异步编程?串行/并行/异常处理常用 API?
答案:
CompletableFuture 是 JDK 8 提供的异步编程工具,基于 ForkJoinPool.commonPool()(线程数 = CPU 核数 - 1)。
核心 API:
- 创建 :
supplyAsync(Supplier)有返回值,runAsync(Runnable)无返回值,可指定自定义线程池。 - 串行 :
thenApply:处理结果并转换;thenAccept:消费结果无返回;thenRun:不依赖结果,执行新任务。
- 并行组合 :
thenCombine:两任务并行,都完成后合并结果;applyToEither:两任务谁先完成用谁的结果;allOf:等待所有完成;anyOf:任意一个完成。
- 异常处理 :
exceptionally(ex -> fallback):捕获异常返回默认值;handle((result, ex) -> ...):统一处理正常和异常结果;whenComplete:类似finally,不改变结果。
工程实践:
- 必须自定义线程池,禁止默认
ForkJoinPool(易打满导致系统阻塞); - 注意
thenApply等回调方法中抛异常会导致后续链路中断,应配合exceptionally或handle。
三、JVM(6 道)
18. ⭐ JVM 内存模型(运行时数据区)?JDK 8 元空间替代永久代的原因?
答案:
运行时数据区:
- 程序计数器:线程私有,记录当前执行字节码行号,唯一无 OOM 区域。
- 虚拟机栈 :线程私有,存储栈帧(局部变量表、操作数栈、动态链接、方法返回地址)。
StackOverflowError(深度过大)/OutOfMemoryError(无法扩展)。 - 本地方法栈:为 Native 方法服务。
- 堆:线程共享,存放对象实例和数组。分代:新生代(Eden、Survivor0、Survivor1)和老年代。
- 方法区(元空间 Metaspace):存储类信息、常量、静态变量、即时编译器编译后的代码。JDK 8 前为永久代(PermGen),后为元空间。
元空间替代永久代的原因:
- OOM 风险 :永久代大小固定(
-XX:MaxPermSize),动态生成类(如 CGLIB、反射、OSGi)容易溢出;元空间使用本地内存,默认只受物理内存限制。 - HotSpot 与 JRockit 融合:JRockit 无永久代,为统一虚拟机架构。
- GC 效率:永久代 GC 效率低,元空间使用独立的类元数据回收机制。
19. ⭐ 垃圾回收算法:标记-清除、标记-整理、复制算法的原理与优缺点?
答案:
标记-清除(Mark-Sweep):
- 先标记所有可达对象,再清除未标记对象。
- 优点:简单,无需移动对象。
- 缺点:产生内存碎片,分配大对象时可能失败;标记和清除效率都不高。
标记-整理(Mark-Compact):
- 标记后,将存活对象向一端移动,清理边界外内存。
- 优点:无内存碎片。
- 缺点:移动对象成本高,需更新引用地址;STW(Stop The World)时间长。适合老年代。
复制(Copying):
- 将内存分为两块,每次只用一块,GC 时将存活对象复制到另一块,整体清理当前块。
- 优点:简单高效,无碎片。
- 缺点:内存利用率仅 50%。适合新生代(Eden + Survivor,比例 8:1:1,利用率 90%)。
20. ⭐ CMS、G1、ZGC 垃圾收集器的原理、适用场景、优缺点?
答案:
CMS(Concurrent Mark Sweep):
- 目标:最短停顿时间。
- 阶段:初始标记(STW,极短)→ 并发标记 → 重新标记(STW,比初始标记长)→ 并发清除。
- 缺点 :
- CPU 敏感:并发阶段占用 CPU 资源;
- 浮动垃圾:并发清理时产生的新垃圾需预留空间,否则触发 "Concurrent Mode Failure",退化为 Serial Old;
- 内存碎片:标记-清除算法导致,可能提前触发 Full GC。
- 适用:低延迟、堆内存 < 4G 的老年代收集器(JDK 9 已废弃)。
G1(Garbage First):
- 目标 :在可预测的停顿时间模型下(
-XX:MaxGCPauseMillis)实现高吞吐。 - 原理:将堆划分为多个等大的 Region(1~32MB),跟踪每个 Region 的垃圾价值(回收空间/耗时),优先回收价值最大的 Region。
- 阶段:初始标记 → 并发标记 → 最终标记 → 筛选回收(STW,并行复制)。
- 优点:可预测停顿、无内存碎片(整体看是标记-整理,局部看是复制)。
- 适用:大堆内存(> 6G),JDK 9+ 默认收集器。
ZGC(JDK 11+,生产可用 JDK 15+):
- 目标:TB 级堆内存下停顿时间 < 10ms。
- 核心技术 :
- 染色指针:将标记信息存储在指针高位,无需额外内存空间;
- 读屏障:在读取引用时完成指针重映射;
- 并发整理: relocation 阶段完全并发,STW 极短。
- 适用:超大内存、对延迟极度敏感的场景(如金融交易、游戏服务器)。
21. 🔥 对象晋升老年代的时机?触发 Full GC 的条件?
答案:
对象晋升老年代:
- 年龄阈值 :对象在 Survivor 区每熬过一次 Minor GC,年龄 +1,默认达到 15 岁 (
-XX:MaxTenuringThreshold)晋升。 - 动态年龄判断: Survivor 区中相同年龄对象大小总和超过 Survivor 空间的一半,则大于等于该年龄的对象直接晋升。
- 大对象直接进入老年代 :
-XX:PretenureSizeThreshold(仅 Serial 和 ParNew 有效),避免在 Eden 和 Survivor 间频繁拷贝。 - 空间分配担保:Minor GC 前,JVM 检查老年代最大连续空间是否大于新生代所有对象总空间。若不足,看是否允许担保失败;若不允许或历史上担保失败,则先 Full GC。
触发 Full GC 的条件:
- 老年代空间不足;
- 元空间不足;
System.gc()调用(建议 JVM 参数-XX:+DisableExplicitGC禁用);- CMS 的 Concurrent Mode Failure / Promotion Failure;
- Minor GC 时空间分配担保失败。
22. 🔥 JVM 调优常用参数?如何排查 OOM?
答案:
常用参数:
-Xms/-Xmx:堆初始/最大内存,建议设为相同值,避免运行时扩容。-Xmn:新生代大小,通常为堆的 1/3 ~ 1/4。-XX:MetaspaceSize/-XX:MaxMetaspaceSize:元空间初始/最大。-XX:+UseG1GC/-XX:MaxGCPauseMillis=200:G1 收集器及目标停顿时间。-XX:+HeapDumpOnOutOfMemoryError/-XX:HeapDumpPath:OOM 时自动 dump 堆内存。
OOM 排查:
- 堆 OOM :
- 现象:
java.lang.OutOfMemoryError: Java heap space。 - 排查:
jmap -dump:format=b,file=... <pid>生成 dump,用 MAT / VisualVM 分析 dominator tree,查找大对象和引用链。 - 解决:检查内存泄漏(静态集合持有对象未释放)、增大堆内存、优化对象生命周期。
- 现象:
- 元空间 OOM :
- 现象:
OutOfMemoryError: Metaspace。 - 排查:检查动态生成类(CGLIB、反射、Groovy 脚本)是否未卸载。
- 解决:增大
MaxMetaspaceSize,检查类加载器泄漏。
- 现象:
- 线程栈 OOM :
- 现象:
Unable to create new native thread。 - 排查:线程数超过系统限制(
ulimit -u)或内存不足。 - 解决:减少线程池大小,检查是否有线程泄漏。
- 现象:
23. 🔥 类加载机制?双亲委派模型?如何打破双亲委派?
答案:
类加载过程:加载 → 验证 → 准备 → 解析 → 初始化。
- 加载 :通过全限定名获取二进制字节流,生成
Class对象。 - 验证:文件格式、元数据、字节码、符号引用验证。
- 准备 :为类变量(
static)分配内存并设零值。 - 解析:符号引用转直接引用。
- 初始化 :执行
<clinit>()方法(静态变量赋值、静态代码块)。
双亲委派模型:
- 类加载器层次:Bootstrap(
%JAVA_HOME%/lib)→ Extension(ext目录)→ Application(classpath)→ 自定义。 - 机制:收到加载请求后,先委派给父加载器,父加载器无法完成时才自己加载。
- 好处 :防止核心类被篡改(如自定义
java.lang.String),避免重复加载。
打破双亲委派:
- Tomcat :Web 应用隔离需求,不同 Web 应用可能依赖同一库的不同版本。Tomcat 为每个 Web 应用创建独立的
WebAppClassLoader,先尝试自己加载,失败再委派,实现类隔离。 - SPI 机制 :如 JDBC,
DriverManager由 BootstrapClassLoader 加载,但具体驱动实现由 AppClassLoader 加载。通过Thread.currentThread().getContextClassLoader()获取线程上下文类加载器(默认为 AppClassLoader)来加载实现类,破坏了双亲委派。 - OSGi:模块化热部署,每个 Bundle 有自己的类加载器,网状加载结构。
四、MySQL(8 道)
24. ⭐ MySQL 索引底层数据结构?为什么用 B+树?聚簇索引 vs 非聚簇索引?
答案:
为什么用 B+树:
- B 树 vs B+树:B 树非叶子节点也存数据,导致节点能存储的键值少,树更高;B+树非叶子节点只存索引键,数据都在叶子节点,节点扇出更大,树更矮胖,IO 次数更少。
- 顺序访问:B+树叶子节点通过双向链表连接,范围查询和排序只需顺序遍历叶子节点,效率高。
- 稳定查询:所有查询路径长度相同(从根到叶子),性能稳定。
- 哈希索引:仅支持等值查询,不支持范围查询和排序,且哈希冲突时性能下降。
聚簇索引 vs 非聚簇索引:
- 聚簇索引:数据行和索引存储在一起,叶子节点就是数据页。InnoDB 表必有聚簇索引(主键索引),若无主键则选第一个非空唯一索引,否则隐式创建 6 字节的 row_id。
- 非聚簇索引(二级索引) :叶子节点存储主键值,查询时需回表(根据主键再去聚簇索引查数据)。
- 覆盖索引:查询字段都在二级索引中,无需回表,性能极高。
25. ⭐ 索引失效场景?
答案:
- 违反最左前缀法则 :联合索引
(a,b,c),查询条件缺少a或只有b,c时失效。 - 范围查询右侧列失效 :
where a=1 and b>2 and c=3,c的索引失效(b是范围查询,其后的列无法使用索引)。 - 索引列参与计算/函数 :
where left(name, 3) = 'abc'或where age + 1 = 18。 - 隐式类型转换 :字段是
varchar,传入数字(where phone = 13800138000),MySQL 会隐式转换字段类型,导致索引失效。 - LIKE 以 % 开头 :
where name like '%abc'无法使用索引;like 'abc%'可以。 - OR 条件:OR 两侧只要有一侧未使用索引,整体失效(MySQL 8.0 索引合并优化有所改善)。
- 不等于(!= / <>)和 NOT IN:通常不走索引(选择性极低时可能走)。
- IS NULL / IS NOT NULL:取决于数据分布,若大部分为 NULL 或大部分非 NULL,可能选择全表扫描。
26. ⭐ 事务 ACID?四种隔离级别?脏读、不可重复读、幻读?
答案:
ACID:
- 原子性(Atomicity):事务要么全做,要么全不做,基于 Undo Log 实现。
- 一致性(Consistency):事务执行前后,数据库从一个一致状态到另一个一致状态(由原子性、隔离性、持久性共同保证)。
- 隔离性(Isolation):事务间互不干扰,基于锁和 MVCC 实现。
- 持久性(Durability):事务提交后永久生效,基于 Redo Log 实现(WAL,Write-Ahead Logging)。
隔离级别:
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 |
| READ COMMITTED | ❌ 不会 | ✅ 可能 | ✅ 可能 |
| REPEATABLE READ(默认) | ❌ 不会 | ❌ 不会 | ❌ 不会(InnoDB) |
| SERIALIZABLE | ❌ 不会 | ❌ 不会 | ❌ 不会 |
- 脏读:读到其他事务未提交的数据;
- 不可重复读:同一事务内两次读取同一行,数据被其他事务修改并提交;
- 幻读 :同一事务内两次范围查询,结果集行数不同(其他事务插入/删除并提交)。InnoDB 的 RR 通过 临键锁(Next-Key Lock) 解决幻读。
27. ⭐ MVCC 实现原理?RC 与 RR 级别下 ReadView 生成时机差异?
答案:
MVCC(Multi-Version Concurrency Control):
- 为每条记录维护多个版本,通过 Undo Log 版本链 实现。每行记录隐藏两个字段:
DB_TRX_ID(最近修改的事务 ID)和DB_ROLL_PTR(回滚指针,指向 Undo Log)。 - 事务修改数据时,不直接覆盖原数据,而是生成新版本,旧版本通过 Undo Log 链访问。
ReadView(一致性视图):
- 事务执行快照读时生成,包含:
creator_trx_id:创建该视图的事务 ID;m_ids:生成视图时未提交的事务 ID 列表;min_trx_id:m_ids中最小值;max_trx_id:下一个即将分配的事务 ID。
可见性判断 :遍历 Undo Log 链,找到第一个满足 DB_TRX_ID < min_trx_id 或 DB_TRX_ID == creator_trx_id 或 DB_TRX_ID > max_trx_id 或 DB_TRX_ID not in m_ids 的版本。
RC vs RR 差异:
- RC(Read Committed) :每次 SELECT 都生成新的 ReadView,所以能读到其他事务已提交的最新数据,导致不可重复读。
- RR(Repeatable Read) :事务第一次 SELECT 时生成 ReadView,后续复用,保证事务内多次读取结果一致。
28. ⭐ InnoDB 锁机制:行锁、间隙锁、临键锁?幻读如何通过临键锁解决?
答案:
行锁(Record Lock) :锁定索引记录本身。SELECT ... FOR UPDATE 对唯一索引等值查询且记录存在时,退化为行锁。
间隙锁(Gap Lock) :锁定索引记录之间的间隙,防止其他事务在间隙中插入数据。SELECT ... FOR UPDATE 对唯一索引等值查询且记录不存在时,或范围查询时,会加间隙锁。
临键锁(Next-Key Lock) :行锁 + 间隙锁,锁定记录及其前面的间隙。是 InnoDB 默认的行锁算法(RR 级别)。
解决幻读:
- 在 RR 级别执行
SELECT ... FOR UPDATE范围查询时,InnoDB 对查询范围内的记录加临键锁,不仅锁定已有记录,还锁定记录前的间隙,阻止其他事务插入新记录,从而避免幻读。 - 注意:普通
SELECT(快照读)通过 MVCC 解决幻读;当前读(FOR UPDATE、LOCK IN SHARE MODE)通过临键锁解决。
29. 🔥 慢查询优化思路?EXPLAIN 分析关注哪些字段?
答案:
优化思路:
- 定位慢 SQL :开启慢查询日志(
slow_query_log),设置阈值(long_query_time)。 - 分析执行计划 :使用
EXPLAIN或EXPLAIN ANALYZE(MySQL 8.0.18+)。 - 索引优化:添加缺失索引、优化联合索引顺序、消除索引失效。
- SQL 改写 :避免
SELECT *,减少回表;拆分大 SQL;用连接替代子查询。 - 架构优化:读写分离、分库分表、引入缓存。
EXPLAIN 关键字段:
- type :访问类型,从优到劣:
system > const > eq_ref > ref > range > index > ALL。至少应达到range,ALL表示全表扫描。 - key:实际使用的索引。
- rows:预估扫描行数,越小越好。
- Extra :
Using index:覆盖索引,无需回表;Using where:Server 层过滤;Using filesort:需要额外排序,性能差;Using temporary:使用临时表,常见于GROUP BY、DISTINCT;Using index condition:索引下推(ICP),在存储引擎层过滤数据,减少回表。
30. 🔥 分库分表方案?ShardingKey 如何选择?分页查询问题?
答案:
拆分方式:
- 垂直拆分:按业务模块拆分(用户库、订单库),解决单库数据量过大、高并发压力,但会引入分布式事务和跨库 Join。
- 水平拆分:按某种规则将同一表数据拆到多库多表(如 user_0 ~ user_7),解决单表数据量过大(> 5000 万行或 > 100GB)。
ShardingKey 选择原则:
- 高频查询字段:如用户 ID、订单 ID,保证 80% 查询能直接定位到分片;
- 数据均匀:避免热点(如按时间分片导致近期分片压力过大);
- 避免跨片查询:常用方案:哈希取模(均匀但扩容麻烦)、一致性哈希(平滑扩容)、范围分片(易扩容但可能热点)。
分页查询问题:
- 分页需聚合多个分片的结果,再排序取页,深分页时性能极差(如
LIMIT 1000000, 10)。 - 解决方案 :
- 禁止深分页:产品层限制最大页码;
- 游标分页 :使用上次查询的最大 ID 作为条件(
where id > last_id limit 10); - 二次查询法 :先查各分片
LIMIT offset, n,汇总排序后确定全局 offset,再回查具体数据; - 全局索引表:将排序字段和 ID 建全局索引表,先查索引表再回查详情。
31. 🔥 主从复制原理?读写分离延迟问题如何解决?
答案:
主从复制原理:
- Master 将变更写入 Binlog(逻辑日志,有三种格式:Statement、Row、Mixed)。
- Slave 的 IO Thread 连接 Master,读取 Binlog 写入本地 Relay Log。
- Slave 的 SQL Thread 重放 Relay Log,完成数据同步。
Binlog 格式:
- Statement :记录 SQL 原文,日志小,但某些函数(
UUID()、NOW())会导致主从不一致; - Row:记录每行变更前后数据,一致性高,日志大;
- Mixed:默认 Statement,特殊场景自动切换 Row。
延迟问题解决方案:
- 强制走主库:对一致性要求极高的查询(如刚修改后立刻查),通过 Hint 或框架层标记走 Master。
- 半同步复制 :
rpl_semi_sync_master_enabled,至少一个 Slave 收到并写入 Relay Log 后才返回成功,降低延迟概率(但牺牲一定性能)。 - 并行复制 :MySQL 5.7+ 基于 Group Commit 的
slave_parallel_workers,按库或按事务组并行重放。 - 数据同步层:引入 Canal 监听 Binlog,将变更同步到 Redis/ES,读请求直接走缓存或搜索引擎。
五、Redis(6 道)
32. ⭐ Redis 五种基本数据类型的底层实现?
答案:
- String :底层是 SDS(Simple Dynamic String) ,结构包含
len、alloc、flags、buf[]。相比 C 字符串:O(1) 获取长度、二进制安全、预分配空间减少内存重分配、惰性释放。 - List :JDK 7 前是双向链表 + ziplist;JDK 7 后是 quicklist(双向链表 + ziplist 节点),兼顾插入删除和内存紧凑。
- Hash :数据量少时用 ziplist (紧凑列表),数据量多转为 hashtable(字典,两个 dictht,渐进式 rehash)。
- Set :整数元素且数量少时用 intset (有序整数数组),否则用 hashtable(值存为 key,value 为 NULL)。
- ZSet(Sorted Set) :数据量少用 ziplist ,数据量多转为 skiplist + hashtable(跳表按 score 排序,hashtable 存储 member→score 映射,实现 O(1) 查 score 和 O(logN) 范围查询)。
33. ⭐ Redis 持久化:RDB 与 AOF 原理、优缺点?混合持久化?
答案:
RDB(Redis Database):
- 原理:定时 fork 子进程,生成某一时刻的全量内存快照(二进制压缩文件)。
- 优点:恢复速度快,文件紧凑适合备份。
- 缺点:可能丢失最后一次快照后的数据;fork 时若数据量大,会阻塞主进程(Copy-On-Write 机制下,修改共享页会触发缺页中断复制内存)。
AOF(Append Only File):
- 原理:记录每条写命令,重启时重放命令恢复数据。
- 优点:数据安全性高(可配置每秒同步或每次写入同步)。
- 缺点 :文件大,恢复慢;AOF 重写(
BGREWRITEAOF)时通过 fork 子进程读取当前内存数据生成新 AOF 文件,期间新命令写入 AOF 重写缓冲区,重写完成后追加。
混合持久化(Redis 4.0+):
- AOF 重写时,前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量命令。
- 结合 RDB 恢复速度和 AOF 数据安全性,生产环境推荐开启。
34. ⭐ 缓存穿透、缓存击穿、缓存雪崩的原因与解决方案?
答案:
缓存穿透:查询一个数据库和缓存都不存在的数据,导致每次请求都打到数据库。
- 解决 :
- 布隆过滤器:在缓存前加布隆过滤器,快速判断 key 是否可能存在;
- 缓存空值:对不存在的 key 也缓存一个短时效的空值(注意占用内存和短期不一致)。
缓存击穿:热点 key 突然过期,大量请求同时打到数据库。
- 解决 :
- 互斥锁:获取锁后查库并回写缓存,其他线程等待或降级;
- 逻辑过期:不设置 TTL,通过逻辑时间判断数据是否过期,过期时异步更新;
- 热点 key 预加载:定时续期,避免过期。
缓存雪崩:大量 key 同时过期,或 Redis 宕机,导致数据库压力骤增。
- 解决 :
- 随机过期时间:在基础 TTL 上加随机偏移,避免集中过期;
- 多级缓存:本地缓存(Caffeine)+ Redis + 数据库;
- 熔断降级:数据库压力过大时,熔断部分请求返回默认值;
- 高可用:Redis Cluster / Sentinel 保证服务可用性。
35. ⭐ Redis 分布式锁的实现?RedLock 算法及争议?
答案:
基础实现:
SET lock_key unique_value NX PX 30000
NX:仅当 key 不存在时设置;PX:设置过期时间,防止死锁;unique_value:通常为 UUID + 线程 ID,释放锁时通过 Lua 脚本判断值是否匹配再删除,防止误删他人锁。
看门狗机制(Redisson):
- 获取锁成功后,启动后台线程,每隔锁过期时间的 1/3(如 10s)续期一次,直到业务完成释放锁。
RedLock 算法(Redis 作者提出):
- 在 N 个(通常 5 个)独立 Redis 实例上依次获取锁,总耗时小于锁过期时间,且成功获取多数(N/2+1)实例的锁,才算获取成功。
- 争议 :
- 时钟漂移:若某 Redis 节点时钟超前,锁可能提前过期,导致两个客户端同时持有锁;
- 网络延迟:获取锁的耗时难以精确控制;
- Martin Kleppmann 的批评:RedLock 依赖系统时钟,而分布式系统中时钟不可靠,建议使用基于共识的锁(如 ZooKeeper、etcd)。
工程建议:非极端场景用 Redisson 单节点/主从锁即可;对一致性要求极高的场景(如金融),使用 ZooKeeper 的临时顺序节点。
36. 🔥 Redis 主从复制、哨兵、Cluster 集群模式的原理与区别?
答案:
主从复制:一主多从,主写从读。从节点启动时全量同步(RDB),之后增量同步(复制积压缓冲区)。无法自动故障转移。
哨兵(Sentinel):
- 监控主从节点健康;
- 自动故障转移:当主节点宕机,选举一个从节点晋升为主节点(基于 Raft 算法),并通知客户端更新主节点地址;
- 最少需要 3 个哨兵节点,防止脑裂。
- 缺点:只有一个主节点写,无法水平扩展写性能。
Cluster 集群:
- 数据分片:将 16384 个哈希槽分配到多个主节点,通过 CRC16(key) % 16384 确定槽位。
- 水平扩展:支持动态增删节点,槽位可在线迁移。
- 高可用:每个主节点至少一个从节点,主节点故障时从节点自动晋升。
- 客户端路由 :客户端缓存槽位映射,可直接定位到目标节点;若槽位迁移,节点返回
-MOVED或-ASK重定向。
37. 🔥 如何保证缓存与数据库一致性?
答案:
Cache Aside(旁路缓存,最常用):
- 读:先读缓存,命中返回;未命中读数据库,写入缓存后返回。
- 写 :先更新数据库,再删除缓存(非更新缓存)。
- 为什么删除而非更新:并发写时,两个线程更新顺序可能错乱,导致缓存与数据库长期不一致;删除缓存下次读时自然加载最新值。
延迟双删:
- 先删除缓存 → 更新数据库 → 睡眠一定时间(如 500ms,大于主从延迟)→ 再次删除缓存。
- 解决并发读写导致的不一致:线程 A 更新数据库期间,线程 B 读取旧数据写入缓存,第二次删除可清除脏缓存。
监听 Binlog(最终一致性):
- 通过 Canal / Maxwell 监听 MySQL Binlog,异步更新/删除缓存。
- 优点:业务代码解耦,缓存操作在统一链路;
- 缺点:有毫秒级延迟,适合对一致性要求不极端的场景。
强一致性方案:
- 使用分布式锁(Redis RedLock / ZooKeeper),保证读写互斥,性能差,仅用于库存等强一致场景。
六、Spring & MyBatis(5 道)
38. ⭐ Spring 如何解决循环依赖(setter 注入)?三级缓存机制?为什么构造器注入不行?
答案:
解决前提 :仅支持 setter / field 注入 的单例 Bean,构造器注入 和prototype不支持。
三级缓存:
- singletonObjects(一级缓存):存放完全初始化好的 Bean(成品)。
- earlySingletonObjects(二级缓存):存放实例化但未赋值的 Bean(半成品),用于解决循环依赖。
- singletonFactories(三级缓存) :存放生成早期引用的
ObjectFactory,用于生成代理对象。
解决流程(A ↔ B):
- 创建 A,实例化后(未赋值)将
ObjectFactory放入三级缓存; - A 属性赋值时发现需要 B,开始创建 B;
- B 实例化后放入三级缓存,属性赋值时发现需要 A;
- 从三级缓存获取 A 的
ObjectFactory,生成早期引用放入二级缓存,B 完成注入和初始化; - B 放入一级缓存,A 继续完成属性赋值和初始化。
为什么需要三级缓存(而非二级):
- 若 A 需要代理(AOP),早期引用必须是代理对象而非原始对象。三级缓存中的
ObjectFactory可在需要时调用BeanPostProcessor生成代理对象,放入二级缓存。若只有二级缓存,无法区分是否需要提前创建代理。
构造器注入不行:
- 构造器调用时对象尚未实例化,无法提前暴露半成品。Spring 在调用构造器前没有任何对象引用可放入缓存,无法打破循环。
39. ⭐ Spring IoC 容器启动流程?Bean 的生命周期?
答案:
IoC 容器启动流程:
- 加载配置 :读取 XML / 注解 / JavaConfig,封装为
BeanDefinition。 - BeanDefinition 注册 :存入
BeanDefinitionRegistry(ConcurrentHashMap)。 - BeanFactoryPostProcessor 执行 :如
PropertySourcesPlaceholderConfigurer解析占位符,修改BeanDefinition。 - Bean 实例化 :调用构造器创建对象(
createBeanInstance)。 - 属性赋值 :
populateBean,注入依赖(DI)。 - 初始化 :
initializeBean:Aware接口回调(BeanNameAware、ApplicationContextAware);BeanPostProcessor.postProcessBeforeInitialization;@PostConstruct/InitializingBean.afterPropertiesSet()/ 自定义 init-method;BeanPostProcessor.postProcessAfterInitialization(AOP 代理在此生成)。
- 使用:Bean 就绪。
- 销毁 :容器关闭时,
@PreDestroy/DisposableBean.destroy()/ 自定义 destroy-method。
40. ⭐ Spring AOP 原理?JDK 动态代理 vs CGLIB 代理?@Transactional 失效场景?
答案:
AOP 原理 :基于 动态代理,在运行期将增强逻辑织入目标对象。
- 若目标类实现了接口,默认使用 JDK 动态代理 (生成接口实现类,持有目标对象引用,通过
InvocationHandler.invoke拦截方法); - 若未实现接口,使用 CGLIB (生成目标类的子类,通过
MethodInterceptor.intercept拦截,不能代理final类和final方法)。 - 可通过
@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用 CGLIB。
JDK vs CGLIB:
- JDK 代理只能代理接口方法,反射调用性能略低;
- CGLIB 通过 FastClass 机制(生成方法索引表)避免反射,调用性能更高,但生成代理类耗时更长。
@Transactional 失效场景:
- 非 public 方法:Spring AOP 基于代理,只能拦截 public 方法;
- 同类内部调用 :
this.method()不走代理,事务不生效。应注入自身代理或拆分到另一个 Bean; - 异常被吞掉 :默认只回滚
RuntimeException和Error,若捕获异常未抛出,或抛出受检异常,事务不回滚; - 异步方法 :
@Async需在事务外调用,否则事务可能未提交; - 数据库引擎不支持:如 MyISAM 不支持事务。
41. 🔥 Spring Boot 自动装配原理?
答案:
核心注解链 :
@SpringBootApplication → @EnableAutoConfiguration → @Import(AutoConfigurationImportSelector.class)
流程:
AutoConfigurationImportSelector实现DeferredImportSelector,在 BeanDefinition 加载阶段被调用。- 读取所有
META-INF/spring.factories(Spring Boot 2.7+ 改为META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)中配置的EnableAutoConfiguration类全限定名。 - 通过
SpringFactoriesLoader加载候选配置类列表。 - 利用 条件注解 过滤:
@ConditionalOnClass:类路径存在某类时生效;@ConditionalOnMissingBean:容器中没有该 Bean 时生效;@ConditionalOnProperty:配置文件中某属性满足条件时生效。
- 符合条件的配置类被注册到容器,完成自动装配。
自定义 Starter:
- 创建自动配置类,用
@ConditionalOnClass等控制条件; - 在
META-INF/spring.factories中注册; - 提供
spring-configuration-metadata.json实现 IDE 配置提示。
42. 🔥 MyBatis 执行流程?#{} 与 ${} 区别?一级缓存与二级缓存?
答案:
执行流程:
- 读取配置文件(
mybatis-config.xml/ Spring Boot 的application.yml),构建SqlSessionFactory; - 通过
SqlSessionFactory创建SqlSession(非线程安全,每次请求新建); SqlSession调用 Mapper 接口方法,通过 JDK 动态代理 生成 MapperProxy;MapperProxy解析注解/XML,生成MappedStatement;Executor执行 SQL,参数处理(ParameterHandler)、结果映射(ResultSetHandler);- 若开启二级缓存,
Executor先查二级缓存,再查一级缓存,最后查数据库。
#{} vs ${}`:
#{}:预编译参数占位符,MyBatis 将其替换为?,通过PreparedStatement设置参数,防 SQL 注入。${}:字符串直接替换,用于动态表名、列名等场景,存在 SQL 注入风险,必须严格校验输入。
缓存:
- 一级缓存 :
SqlSession级别,默认开启。同一会话内多次相同查询命中缓存。SqlSession关闭后清空。 - 二级缓存 :
Mapper Namespace级别,需手动开启(<cache/>或@CacheNamespace)。多个SqlSession共享,基于TransactionalCacheManager,提交时才真正放入缓存。 - 注意 :二级缓存实体类必须实现
Serializable,且分布式环境下建议使用 Redis 替代 MyBatis 二级缓存。
七、消息队列(5 道)
43. ⭐ Kafka 高吞吐原理?ISR 机制?
答案:
高吞吐四大核心:
- 顺序写磁盘:Kafka 消息追加到日志文件末尾,顺序写性能接近内存写(600MB/s),远超随机写(100KB/s)。
- 页缓存(Page Cache):数据先写入操作系统页缓存,由 OS 异步刷盘,减少 JVM GC 和用户态/内核态拷贝。
- 零拷贝(Zero-Copy) :消费者读取时,通过
sendfile系统调用,数据直接从 Page Cache 发送到网卡,绕过应用缓冲区,减少 2 次 CPU 拷贝和 2 次上下文切换。 - 批量压缩 :生产者批量发送(
batch.size、linger.ms),支持 Snappy、LZ4、GZIP 压缩,减少网络 IO。
ISR(In-Sync Replicas)机制:
- ISR 是与 Leader 保持同步的副本集合。Leader 维护每个 Follower 的 LEO(Log End Offset)。
- 只有 ISR 中的副本才有资格竞选 Leader(
unclean.leader.election.enable=false时)。 - Follower 若延迟超过
replica.lag.time.max.ms,则被踢出 ISR;追上后重新加入。 - 生产者
acks=all时,需 ISR 中所有副本确认才返回成功,保证数据可靠性。
44. ⭐ RocketMQ 事务消息实现原理?顺序消息如何实现?
答案:
事务消息(半消息机制):
- 发送半消息 :生产者发送事务消息到 Broker,此时消息对消费者不可见(存在
RMQ_SYS_TRANS_HALF_TOPIC)。 - 执行本地事务 :生产者回调
executeLocalTransaction,执行数据库操作等本地事务。 - 提交或回滚 :
- 本地事务成功,发送
COMMIT,Broker 将消息从半消息队列移到目标 Topic,消费者可见; - 本地事务失败,发送
ROLLBACK,Broker 删除半消息。
- 本地事务成功,发送
- 事务回查 :若生产者未返回状态或宕机,Broker 定时回查(
checkLocalTransaction),根据本地事务状态决定提交或回滚。
顺序消息:
- 全局顺序:Topic 只有一个队列,所有消息严格有序,吞吐量低。
- 分区顺序(局部顺序) :同一业务 ID(如订单 ID)的消息发送到同一队列。生产者通过
MessageQueueSelector根据hash(orderId) % queueNum选择队列;消费者使用MessageListenerOrderly,加锁消费保证单队列内顺序。
45. ⭐ 消息队列如何保证消息不丢失?
答案:
生产者端:
- Kafka:
acks=all(Leader + ISR 确认),开启重试(retries),使用幂等生产者(enable.idempotence=true,PID + Sequence Number 去重)。 - RocketMQ:同步发送(
SendResult),事务消息保证本地事务与消息发送一致。
Broker 端:
- Kafka:多副本(
replication.factor >= 3),最小 ISR 数(min.insync.replicas=2),刷盘策略(flush.messages/flush.ms)。 - RocketMQ:同步刷盘(
flushDiskType=SYNC_FLUSH),主从同步复制(brokerRole=SYNC_MASTER)。
消费者端:
- 手动 ACK(Kafka 关闭自动提交
enable.auto.commit=false,RocketMQ 消费成功才返回CONSUME_SUCCESS); - 消费逻辑幂等设计(数据库唯一键、Redis setnx、业务状态机幂等)。
46. 🔥 消息幂等性如何保证?消息积压如何处理?死信队列?
答案:
幂等性保证:
- 数据库唯一索引:如订单号 + 消息 ID 建唯一键,重复插入抛异常捕获。
- Redis SETNX :
SET message_id 1 EX 86400 NX,利用原子性和过期时间防重。 - 状态机幂等:如订单状态只能 待支付→已支付→已发货,重复消息不会改变状态。
- Token 机制:服务端预分配 Token,消费时校验并删除。
消息积压处理:
- 紧急扩容:增加消费者实例(需保证消费者逻辑无状态)。
- 跳过非关键消息:若部分消息可丢弃,临时跳过堆积时间段的消息。
- 优化消费逻辑:批量消费、异步处理、减少数据库交互。
- 重置消费位点:极端情况下,将消费位点重置到最新,牺牲部分数据实时性(需业务可接受)。
死信队列(DLQ):
- 消息消费失败达到最大重试次数后,转入死信队列。需监控 DLQ,人工或自动处理异常消息,分析失败原因(数据异常、Bug、依赖服务故障)。
47. 🔥 Kafka 与 RocketMQ 的区别?各自适用场景?
答案:
| 维度 | Kafka | RocketMQ |
|---|---|---|
| 设计定位 | 高吞吐日志流处理 | 金融级可靠消息、业务消息 |
| 吞吐量 | 极高(百万级 TPS) | 高(十万级 TPS) |
| 延迟 | 高吞吐下 ms 级 | 低延迟(ms 级) |
| 消息顺序 | 单分区有序 | 支持全局/分区顺序 |
| 事务消息 | 支持(幂等生产者+事务API) | 原生支持(半消息+回查) |
| 延迟消息 | 需外部实现 | 原生支持 18 个级别 |
| 消息轨迹 | 需外部实现 | 原生支持 |
| 协议生态 | 生态丰富,大数据标配 | 阿里生态,Java 友好 |
适用场景:
- Kafka:日志采集、实时计算(Flink/Spark)、事件溯源、大数据管道。
- RocketMQ:电商交易、金融支付、分布式事务(最终一致性)、需要延迟消息和消息轨迹的业务系统。
八、计算机网络(5 道)
48. ⭐ TCP 三次握手、四次挥手?为什么不是两次握手?TIME_WAIT 作用?大量 CLOSE_WAIT 怎么解决?
答案:
三次握手:
- SYN=1, seq=x(客户端 → 服务端)
- SYN=1, ACK=1, seq=y, ack=x+1(服务端 → 客户端)
- ACK=1, seq=x+1, ack=y+1(客户端 → 服务端)
为什么不是两次:
- 防止历史重复连接初始化。若客户端第一个 SYN 延迟到达,服务端直接建立连接,但客户端已放弃该连接,导致服务端资源浪费。
- 同步双方初始序列号(ISN),两次握手只能确认一方的收发能力。
四次挥手:
- FIN=1, seq=u(主动方 → 被动方)
- ACK=1, seq=v, ack=u+1(被动方 → 主动方,半关闭)
- FIN=1, ACK=1, seq=w, ack=u+1(被动方 → 主动方,数据发完)
- ACK=1, seq=u+1, ack=w+1(主动方 → 被动方)
TIME_WAIT(2MSL)作用:
- 保证最后一个 ACK 能被对方收到,若丢失可重传;
- 防止已失效的连接请求报文出现在本连接中(等待网络中残留报文消失)。
大量 CLOSE_WAIT:
- 原因:被动关闭方收到 FIN 后,应用层未调用
close()或socket未关闭,导致无法发送 FIN。 - 解决:检查代码逻辑,确保异常分支也关闭连接;检查是否有阻塞 IO 导致应用无法响应。
49. ⭐ TCP 与 UDP 区别?TCP 如何保证可靠传输?
答案:
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 可靠 | 不可靠 |
| 有序性 | 有序 | 无序 |
| 拥塞控制 | 有 | 无 |
| 首部开销 | 20 字节 | 8 字节 |
| 适用场景 | HTTP、文件传输 | DNS、视频流、游戏 |
TCP 可靠传输机制:
- 确认应答(ACK):接收方返回确认号,告知期望下一个字节序号。
- 超时重传:发送方启动定时器,超时未收到 ACK 则重传。时间通过 RTT 动态计算(Jacobson 算法)。
- 滑动窗口:实现流量控制,允许发送方连续发送多个段而不等待确认,提高吞吐量。
- 流量控制 :接收方通过
Window Size字段告知发送方剩余缓冲区大小,防止发送过快。 - 拥塞控制 :
- 慢启动:cwnd 从 1 开始指数增长;
- 拥塞避免:达到阈值后线性增长;
- 快重传:收到 3 个重复 ACK 立即重传;
- 快恢复:cwnd 降为一半而非 1。
50. ⭐ HTTP/1.1、HTTP/2、HTTP/3(QUIC)的核心改进?HTTPS 握手过程?
答案:
HTTP/1.1:
- 持久连接(
Connection: keep-alive)、管道化(理论存在,实际因队头阻塞很少用)、Host 头、断点续传(Range)。 - 缺陷:队头阻塞(Head-of-Line Blocking)、明文传输、请求头冗余。
HTTP/2:
- 二进制分帧:将请求/响应拆分为帧,多路复用(Multiplexing),一个 TCP 连接并发多个流,解决队头阻塞;
- 头部压缩(HPACK):静态表 + 动态表 + Huffman 编码,减少头部大小;
- 服务器推送:服务端主动推送资源(如 CSS/JS)。
- 缺陷:基于 TCP,TCP 层队头阻塞仍存在(丢包时后续流等待)。
HTTP/3(QUIC):
- 基于 UDP + QUIC 协议,内置 TLS 1.3(1-RTT / 0-RTT 握手);
- 彻底解决队头阻塞:每个流独立传输,某流丢包不影响其他流;
- 连接迁移:通过 Connection ID 标识连接,IP 变化无需重新握手(适合移动端)。
HTTPS 握手(TLS 1.2):
- Client Hello:客户端发送支持的加密套件、随机数(Client Random);
- Server Hello:服务端返回选定加密套件、证书、随机数(Server Random);
- 客户端验证证书,生成 Pre-Master Secret,用公钥加密发送;
- 双方用 Client Random + Server Random + Pre-Master Secret 生成会话密钥(对称加密);
- Finished 消息,后续通信使用对称加密。
TLS 1.3:简化握手,仅需 1-RTT(甚至 0-RTT 恢复会话),废弃 RSA 密钥交换,仅支持 ECDHE,提升安全性和速度。
51. 🔥 输入 URL 到页面显示的全过程?
答案:
- DNS 解析 :浏览器缓存 → OS 缓存(
hosts)→ 本地 DNS 服务器 → 根域名服务器 → 顶级域名服务器 → 权威域名服务器,返回 IP 地址。可能走 DNS 预解析(<link rel="dns-prefetch">)。 - TCP 连接:三次握手建立连接。HTTPS 还需 TLS 握手。
- 发送 HTTP 请求:构建请求行、请求头、Cookie 等,经 TCP/IP 协议栈封装为数据包。
- 网络传输:经过路由器、交换机,可能穿越 NAT,到达服务端。中间经过 CDN 边缘节点时可能直接返回缓存。
- 服务端处理:Nginx 反向代理 → 网关(限流、鉴权)→ 应用服务器(Spring Boot)→ 业务逻辑 → 数据库/缓存查询 → 生成响应。
- 浏览器解析渲染 :
- 解析 HTML 构建 DOM 树;
- 解析 CSS 构建 CSSOM 树;
- DOM + CSSOM → Render Tree;
- Layout(回流):计算元素几何位置;
- Paint(重绘):绘制像素;
- Composite:合成图层,GPU 渲染。
- JS 执行 :
<script>阻塞解析(除非defer/async),操作 DOM 可能触发回流重绘。
52. 🔥 epoll 原理?LT 与 ET 区别?为什么 ET 要配合非阻塞 IO?
答案:
epoll 原理:
- Linux 下 IO 多路复用机制,解决
select/poll的缺陷(fd 数量限制、每次遍历全部 fd、用户态/内核态拷贝)。 - 核心数据结构 :
- 红黑树(rb_tree):存储所有监听的 fd,增删改 O(logN);
- 就绪链表(rdllist) :存储就绪的 fd,通过回调机制(
ep_ptable_queue_proc)将就绪 fd 加入链表; epoll_wait只需检查就绪链表,返回就绪事件,时间复杂度 O(1)。
LT(Level Trigger,水平触发):
- 只要 fd 处于可读/可写状态,
epoll_wait就会一直通知。 - 编程简单,配合阻塞 IO 即可,但可能重复触发。
ET(Edge Trigger,边缘触发):
- 仅在 fd 状态变化时(不可读→可读)通知一次,之后无论多少数据等待,都不再通知,直到再次触发新事件。
- 必须配合非阻塞 IO :因为 ET 只通知一次,应用必须循环
read/write直到返回EAGAIN(数据读完/写满),若用阻塞 IO,最后一次读写会阻塞线程。 - 优点:减少 epoll 事件触发次数,性能更高(高并发服务器如 Nginx 默认 ET)。
九、操作系统 & Linux(3 道)
53. ⭐ 进程、线程、协程的区别?线程切换 vs 进程切换开销?Go 的 GMP 调度模型?
答案:
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 | 用户态轻量级线程 |
| 地址空间 | 独立 | 共享进程空间 | 共享线程空间 |
| 切换开销 | 大(页表、TLB 刷新) | 中(寄存器、栈) | 小(纯用户态) |
| 通信方式 | IPC(管道、共享内存) | 共享内存(需同步) | 直接读写变量 |
| 调度者 | 操作系统内核 | 操作系统内核 | 用户态调度器 |
线程切换 vs 进程切换:
- 进程切换:切换页表(虚拟地址空间)、刷新 TLB、切换内核栈和寄存器,开销大(微秒级)。
- 线程切换:同一进程内线程共享地址空间,只需切换寄存器、程序计数器、栈指针,TLB 无需刷新(或部分刷新),开销小(亚微秒级)。
Go GMP 模型:
- G(Goroutine):轻量级协程,初始栈仅 2KB,可动态伸缩。
- M(Machine):操作系统线程,由 Go 运行时管理。
- P(Processor) :逻辑处理器,维护本地可运行 Goroutine 队列(LRQ),数量默认等于 CPU 核心数(
GOMAXPROCS)。 - 调度流程 :
- 新 Goroutine 优先放入当前 P 的 LRQ;
- M 绑定 P,从 LRQ 取 G 执行;
- LRQ 空时从全局队列(GRQ)或其他 P 偷取(Work Stealing);
- G 阻塞(如系统调用)时,M 与 P 分离,P 可绑定新 M 继续执行其他 G,避免线程阻塞浪费 CPU。
54. 🔥 Linux 零拷贝(sendfile、mmap)原理?DMA 作用?
答案:
传统 IO(4 次拷贝,4 次上下文切换) :
磁盘 → DMA → 内核 Page Cache → CPU 拷贝 → 用户缓冲区 → CPU 拷贝 → Socket 缓冲区 → DMA → 网卡。
mmap + write(4 次上下文切换,3 次拷贝):
mmap将文件映射到用户态虚拟内存,用户态和内核态共享 Page Cache;write时 CPU 将数据从 mmap 区域拷贝到 Socket 缓冲区;- 省去一次内核态到用户态的拷贝,但仍需 CPU 参与拷贝。
sendfile(2 次上下文切换,2 次拷贝,Linux 2.1+):
sendfile(int out_fd, int in_fd, ...)直接在内核态将数据从 Page Cache 拷贝到 Socket 缓冲区;- 用户态完全不参与数据搬运。
sendfile + DMA gather copy(2 次上下文切换,1 次拷贝,Linux 2.4+):
- DMA 控制器直接将数据从 Page Cache 拷贝到网卡,真正的零拷贝(CPU 不触碰数据)。
- 需网卡支持 Scatter-Gather 特性。
DMA(Direct Memory Access):
- 允许外设(磁盘、网卡)直接读写内存,无需 CPU 干预。CPU 只需发送 DMA 指令,之后可处理其他任务,DMA 完成后通过中断通知 CPU。
55. 🔥 常用 Linux 排查命令:top、ps、netstat、iostat、jstack、jmap?
答案:
top:实时查看进程资源占用。关注load average(1/5/15 分钟平均负载,> CPU 核数表示过载)、%CPU、%MEM、RES(实际物理内存)。按1显示各核负载,按H显示线程。ps -ef | grep java/ps -eo pid,pcpu,pmem,comm --sort=-pcpu:查看进程详细信息。netstat -tunlp/ss -tunlp:查看网络连接、监听端口、对应进程。ss比netstat更快(从内核直接读取)。iostat -x 1:查看磁盘 IO。关注%util(接近 100% 表示磁盘饱和)、await(IO 等待时间,> 20ms 说明磁盘压力大)。jstack -l <pid>:打印线程堆栈,排查死锁、线程阻塞、CPU 飙高(配合top -H找到线程 ID,转十六进制定位)。jmap -dump:format=b,file=... <pid>:生成堆内存 dump,配合 MAT 分析内存泄漏;jmap -histo <pid>查看对象统计。jstat -gcutil <pid> 1000:每秒打印 GC 情况,关注YGC、YGCT、FGC、FGCT、GCT。
十、分布式系统 & 微服务(5 道)
56. ⭐ CAP 理论与 BASE 理论?分布式系统如何权衡 CP vs AP?
答案:
CAP 理论:分布式系统无法同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance),最多满足两项。
- 分区容错性 P :网络分区不可避免,必须满足。因此实际在 CP 和 AP 间选择。
BASE 理论:Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性),是 AP 系统的实践指导。
CP vs AP 权衡:
- CP:牺牲可用性,保证强一致。如 ZooKeeper、etcd、HBase。适合金融交易、库存扣减等场景。
- AP:牺牲强一致,保证可用。如 Eureka、Cassandra、DNS。适合社交 feed、配置中心等场景。
- 实际工程 :并非非黑即白,可通过 读写一致性 、因果一致性 等中间态平衡。如电商订单用 CP,商品详情用 AP。
57. ⭐ 分布式事务解决方案:2PC、3PC、TCC、本地消息表、Saga、MQ 事务消息?
答案:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 2PC | 准备阶段(锁定资源)+ 提交阶段 | 强一致,实现简单 | 同步阻塞、单点故障(协调者)、数据锁定时间长 | 传统数据库(XA) |
| 3PC | 引入 CanCommit 预检查 + 超时机制 | 减少阻塞时间 | 网络分区下不一致、复杂度高 | 较少使用 |
| TCC | Try(预留资源)+ Confirm(确认)+ Cancel(撤销) | 无全局锁,性能高 | 业务侵入大,需实现三接口,Confirm/Cancel 需幂等 | 电商库存、优惠券 |
| 本地消息表 | 业务表 + 消息表同库本地事务,定时扫描发送 | 最终一致,可靠 | 需要定时任务,延迟较高 | 异步通知、对账 |
| Saga | 长事务拆分为本地事务,失败时执行补偿操作 | 适合长流程(如旅游预订) | 无隔离性,可能脏读,补偿逻辑复杂 | 业务流程长、需快速响应 |
| MQ 事务消息 | 半消息 + 本地事务 + 回查 | 解耦,最终一致 | 消费方需幂等 | 订单创建后发积分 |
选型建议:
- 强一致且短事务:2PC/XA;
- 高并发、允许短暂不一致:TCC(Seata)、Saga(Seata);
- 异步场景、最终一致:MQ 事务消息、本地消息表。
58. 🔥 分布式 ID 生成方案?Snowflake 原理?时钟回拨问题如何解决?
答案:
常见方案:
- 数据库自增:简单,但单点瓶颈、性能低、暴露数据量。
- UUID:无序、字符串存储空间大、索引效率低。
- Redis 自增 :原子性
INCR,但依赖 Redis 可用性。 - Snowflake(雪花算法):主流方案。
Snowflake 结构(64 位):
- 1 位符号位(0)+ 41 位时间戳(毫秒级,约 69 年)+ 10 位机器 ID(5 位数据中心 + 5 位机器)+ 12 位序列号(每毫秒 4096 个 ID)。
- 趋势递增,Long 类型存储,索引友好。
时钟回拨问题:
- 服务器 NTP 同步或手动调整时间,导致当前时间小于上次生成 ID 的时间戳,可能生成重复 ID。
- 解决方案 :
- 等待:时钟回拨幅度小(< 5ms)时,线程自旋等待;
- 异常抛出:回拨幅度大时抛异常,由上层重试或报警;
- 备用位:借用 1 位机器 ID 作为回拨标识,回拨时切换标识位;
- 美团 Leaf:采用 ZooKeeper 分配趋势递增的号段,不依赖机器时钟。
59. 🔥 限流算法:计数器、滑动窗口、令牌桶、漏桶?Sentinel 熔断降级原理?
答案:
限流算法:
- 计数器:固定窗口,单位时间内计数,超过则拒绝。实现简单,但窗口边界可能突增 2 倍流量(临界问题)。
- 滑动窗口:将窗口细分为多个子窗口,统计最近 N 个子窗口的请求数。平滑流量,但内存占用随子窗口数增加。
- 令牌桶 :以固定速率向桶中放令牌,请求需获取令牌才能通过。允许一定程度的突发流量(桶有余量时),Google Guava
RateLimiter使用此算法。 - 漏桶:请求进入漏桶,以固定速率流出。强制平滑流量,无法应对突发(适合流量整形)。
Sentinel 熔断降级:
- 状态机 :
Closed(关闭)→Open(打开)→Half-Open(半开)。 - Closed:正常放行,统计错误率 / 慢调用比例。
- Open:错误率超过阈值,熔断打开,请求快速失败。经过熔断时长(如 5s)后进入 Half-Open。
- Half-Open:放行少量请求探测,若成功则关闭熔断,失败则重新打开。
- 降级策略:RT 慢调用比例、异常比例、异常数。
60. 🔥 一致性算法 Raft / Paxos 的基本原理?
答案:
Raft(易懂,工程常用) :
将一致性问题分解为三个子问题:
- Leader 选举:节点初始为 Follower,超时未收到心跳则变为 Candidate,发起投票,获得多数票成为 Leader。
- 日志复制:Leader 接收客户端请求,写入本地日志,并行发送给 Followers;多数确认后提交,应用到状态机,再返回客户端。
- 安全性 :通过任期(Term)和日志匹配保证:
- 同一任期内最多一个 Leader;
- 已提交的日志必须存在于新 Leader;
- Follower 只接受任期 >= 当前任期的 Leader 日志。
Paxos(理论奠基):
- 角色:Proposer(提议者)、Acceptor(接受者)、Learner(学习者)。
- 两阶段:
- Prepare:Proposer 生成全局唯一编号 n,请求 Acceptor 承诺不再接受编号 < n 的提案;
- Accept :若多数 Acceptor 响应,Proposer 发送
[n, v],Acceptor 接受(若 n 仍大于已承诺的最大编号)。
- 缺点:难以理解,多 Paxos 实现复杂(需选主、日志连续等)。
工程应用:
- Raft:etcd、Consul、TiKV、RocketMQ Dledger;
- Paxos:Chubby、ZooKeeper(ZAB 类似 Paxos)。
十一、AI 工程化(20 道)
以下为 2026 年字节、阿里、腾讯、美团、小红书等中大厂 AI 应用开发工程师岗位高频考题,涵盖 RAG、Agent、MCP、LLM 网关、推理优化等方向。
61. ⭐ RAG 的核心原理是什么?完整链路包含哪些环节?
答案:
RAG(Retrieval-Augmented Generation,检索增强生成) 是一种将外部知识检索与大模型生成结合的技术架构,解决大模型知识截止、幻觉和私有数据缺失问题。
完整链路(离线 + 在线):
离线阶段(知识入库):
- 文档解析:PDF/Word/网页等非结构化文档提取文本,需处理表格、图片 OCR(如 MinerU)、多栏排版。
- 文档切割(Chunking) :
- 固定长度切割(简单,但易截断语义);
- 递归字符切割(按段落、句子层级);
- 语义切割(基于 Embedding 相似度断点);
- 父子块(Parent-Document Retrieval):小块用于检索,大块用于上下文生成。
- 向量化(Embedding):通过 Embedding 模型(BGE-M3、OpenAI text-embedding-3)将文本转为高维向量。
- 索引构建:写入向量数据库(Milvus、Qdrant、PGVector),构建 HNSW / IVFFLAT 等近似最近邻索引。
在线阶段(查询生成):
- Query 理解:意图识别、Query 改写(扩展、纠错、HyDE)。
- 检索召回:向量相似度搜索(Top-K)+ 稀疏检索(BM25、全文搜索)多路召回。
- 重排序(Rerank):使用 Cross-Encoder(BGE-Reranker)对候选文档精排序,提升相关性。
- 上下文组装:将 Top-N 文档按模板组装为 Prompt 上下文。
- 大模型生成:LLM 基于检索到的上下文生成回答,可配合引用溯源(Citation)。
- 后处理:答案脱敏、格式校验、幻觉检测。
62. 🔥 RAG 中如何解决文档切割导致的语义截断问题?
答案:
问题本质:固定长度切割(如每 500 token)可能将连贯段落切断(如 "Java 内存模型包含..." 被切到两块),导致检索时语义不完整,生成质量下降。
解决方案:
- 语义切割(Semantic Chunking) :
- 按句子/段落边界切割,保证每块语义完整;
- 使用 Embedding 模型计算相邻句子的相似度,相似度低于阈值时作为切割点。
- 递归字符切割(RecursiveCharacterTextSplitter) :
- 按优先级分割符递归切割:
段落 → 句子 → 单词 → 字符,优先保持大粒度语义单元。
- 按优先级分割符递归切割:
- 父子块(Parent-Document Retrieval) :
- 子块(小粒度,如 256 token)用于 Embedding 和检索,保证检索精度;
- 父块(大粒度,如 2048 token,包含子块所在完整段落)用于生成上下文,保证语义完整。
- 检索命中子块后,回查其父块作为 LLM 输入。
- 重叠切割(Overlap) :
- 相邻块保留一定重叠区域(如 50 token),减少边界信息丢失。
- 结构化切割 :
- 对 Markdown、HTML、JSON 等结构化文档,按标题层级(H1/H2/H3)切割,保持文档结构。
63. 🔥 什么是 Query 改写?HyDE 和 Step-Back Prompting 的原理?
答案:
Query 改写(Query Rewriting / Expansion):用户原始查询往往简短、歧义、口语化,直接用于向量检索效果差。Query 改写通过 LLM 或规则将原始 Query 转换为更适合检索的形式。
常见策略:
- Query 扩展:补充同义词、相关术语。如用户问 "JVM 调优",扩展为 "JVM 调优 GC 参数 OOM 排查"。
- HyDE(Hypothetical Document Embeddings,假设文档嵌入) :
- 原理:让 LLM 根据用户 Query 生成一篇假设的理想答案文档,对该文档做 Embedding,再用这个向量去检索真实文档。
- 优势:将 Query 从短文本空间映射到文档空间,解决 Query 与文档的语义鸿沟(如用户问 "怎么解决死锁",生成的假设文档包含 "jstack 排查、避免循环等待" 等关键词,检索更精准)。
- 代价:增加一次 LLM 调用,延迟和成本上升。
- Step-Back Prompting :
- 原理:让 LLM 先退一步,从具体 Query 抽象出高层概念或原理,再基于原理检索。
- 示例:用户问 "我的 Redis 缓存雪崩了怎么办" → 抽象为 "缓存雪崩的成因与解决方案" → 检索通用解决方案文档,而非仅匹配 "我的 Redis" 这种噪声。
- 适用:技术问答、医疗问诊等需要原理推导的场景。
64. 🔥 RAG 的多路召回方案有哪些?如何做 Rerank?
答案:
多路召回(Multi-Channel Retrieval):单一检索方式存在局限,多路召回通过多种策略互补,提升召回率和准确率。
常见召回路:
- 向量召回(Dense Retrieval):基于 Embedding 语义相似度,适合同义改写、概念匹配。使用 HNSW 近似最近邻搜索,召回 Top-K。
- 稀疏召回(Sparse Retrieval):基于 BM25、TF-IDF、倒排索引的关键词匹配,适合专有名词、ID、精确术语。
- 知识图谱召回:对实体关系型查询(如 "张三的直属领导是谁"),通过图数据库(Neo4j)直接查询关系。
- 全文检索:Elasticsearch 分词匹配,适合长文档中的关键词命中。
- 历史会话召回:从对话历史中提取相关上下文,解决指代消解(如 "它有什么缺点" 中的 "它")。
召回结果融合:
- RRF(Reciprocal Rank Fusion) :
score = Σ 1/(k + rank_i),对不同路的结果排名进行加权融合,无需校准分数。 - 加权分数融合:将各路分数归一化后加权求和(需大量数据调参)。
Rerank(重排序):
- 召回阶段追求速度(近似搜索),排序精度有限。Rerank 使用更复杂的 Cross-Encoder 模型(如 BGE-Reranker-v2)对候选文档与 Query 做交互式编码(拼接后输入 Transformer),输出相关性分数,重新排序取 Top-N。
- 工程实践:召回 100 篇 → Rerank 取 Top-10 → 送入 LLM 生成。Rerank 计算量大,但候选集小,延迟可接受。
65. 🔥 如何解决 RAG 中的幻觉问题?
答案:
RAG 虽能缓解幻觉,但若检索文档不相关、LLM 过度发挥或上下文过长,仍会产生幻觉。
解决策略:
- 检索质量提升 :
- 优化 Query 改写和 Embedding 模型,确保召回文档高相关;
- 使用 Rerank 精排,过滤低质量文档;
- 设置相关性阈值,低于阈值的文档不送入 LLM。
- Prompt 工程约束 :
- 明确指令:"请仅基于以下上下文回答问题,若上下文不包含答案,请回答 '我不知道',不要编造"。
- Few-shot 示例:给出生成和拒答的示例。
- 引用溯源(Citation) :
- 要求 LLM 在回答中标注引用来源(如 [1] [2]),用户可追溯到原文验证。
- 实现:在 Prompt 中要求 "每句话后标注来源编号",后处理校验引用编号是否在提供的文档中。
- 答案一致性校验 :
- Self-RAG:让 LLM 生成答案后,再判断答案是否被上下文支持(Generate → Reflect → Retrieve)。
- 多模型验证:用另一个小模型做事实核查(Fact Checking)。
- 护栏(Guardrails) :
- 使用 NeMo Guardrails 或自研规则,对输出做敏感信息、事实一致性、格式合规校验,拦截幻觉回答。
66. ⭐ Function Calling 的原理是什么?Tool Schema 如何设计?
答案:
Function Calling(工具调用) 是大模型与外部系统交互的核心机制,让 LLM 能够"使用工具"完成计算、查询、操作等自身不擅长的任务。
原理:
- 开发者在请求中传入 Tools 定义(JSON Schema 描述可用函数)。
- LLM 根据用户 Query 和 Tools 描述,自主决策是否需要调用工具、调用哪个工具、传入什么参数。
- LLM 返回特殊的
function_call/tool_calls消息(而非直接回答)。 - 应用层接收到调用指令后,执行对应函数,获取结果。
- 将函数执行结果再次送入 LLM,LLM 基于结果生成最终自然语言回答。
Tool Schema 设计(以 OpenAI 为例):
json
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气,温度、湿度、风速",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 '北京'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位"
}
},
"required": ["city"]
}
}
}
Schema 设计原则:
description必须精准具体,这是 LLM 选择工具的核心依据;- 参数类型明确,用
enum限制取值范围; - 必填参数用
required标注; - 工具名使用动词 + 名词(如
query_order、create_ticket)。
67. ⭐ MCP 协议是什么?与 Function Calling 的本质区别?
答案:
MCP(Model Context Protocol) 是由 Anthropic 于 2024 年底提出、2025-2026 年迅速成为行业标准的开放协议,用于标准化 AI 模型与外部工具、数据源之间的连接。
核心定位:
- Function Calling 是模型能力(OpenAI、Claude、Kimi 都支持,但各家 API 格式不同);
- MCP 是通信协议(类似 HTTP 或 USB-C),定义了 AI 应用(Client)与工具提供方(Server)之间的标准交互方式。
本质区别:
| 维度 | Function Calling | MCP |
|---|---|---|
| 层级 | 模型层能力 | 应用层协议 |
| 生态 | 各厂商 API 格式不一 | 统一标准,一次开发多模型通用 |
| 工具发现 | 应用层硬编码工具列表 | Server 自动暴露可用工具,Client 动态发现 |
| 连接方式 | 本地函数或 HTTP 接口 | 支持 stdio、SSE、HTTP 等多种 Transport |
| 安全性 | 依赖应用层实现 | 内置权限管控、沙箱执行 |
| 可复用性 | 每个应用单独对接 | 工具作为独立服务,多应用共享 |
MCP 架构:
- Host:运行 AI 应用的程序(如 Claude Desktop、IDE)。
- Client:Host 内的 MCP 客户端,维护与 Server 的连接。
- Server :提供具体能力的独立进程(如文件系统、数据库、GitHub API),通过
tools/list暴露工具,通过tools/call执行调用。 - Transport:stdio(本地进程)、SSE(Server-Sent Events,远程流式通信)。
工程意义:2026 年大厂已将 MCP 写入 JD,掌握 MCP 意味着你的 Agent 可以像插 USB 一样即插即用接入任意工具生态。
68. 🔥 MCP 的架构设计(Client-Server-Transport)及通信流程?
答案:
三层架构:
- MCP Host:承载 AI 应用的环境(如 Claude Desktop、Cursor、自研 Agent 平台)。一个 Host 可包含多个 Client。
- MCP Client:与 Server 建立 1:1 连接,负责协议握手、能力协商、消息路由。遵循 JSON-RPC 2.0 规范。
- MCP Server:独立进程或服务,封装特定领域能力(如 PostgreSQL、Slack、文件系统)。通过标准接口暴露资源(Resources)、工具(Tools)和提示(Prompts)。
通信流程:
- 初始化 :Client 发送
initialize请求,协商协议版本、能力(如是否支持 Streaming)。 - 工具发现 :Client 发送
tools/list,Server 返回可用工具列表及其 JSON Schema。 - 调用执行 :
- LLM 决定调用工具后,Client 发送
tools/call,携带工具名和参数; - Server 校验参数 Schema,执行本地逻辑(如查询数据库);
- Server 返回执行结果(文本、图片、错误信息)。
- LLM 决定调用工具后,Client 发送
- 资源订阅 :Client 可订阅 Server 的资源变更(如文件修改),Server 通过
notifications/resources/updated推送。
Transport 选型:
- stdio:本地子进程通信,适合本地工具(如文件系统、本地数据库),安全性高(无网络暴露)。
- SSE(Server-Sent Events):基于 HTTP 的单向流式通信,适合远程服务。
- HTTP:无状态请求响应,适合简单场景。
69. ⭐ 什么是 AI Agent?与 Workflow 的本质区别?
答案:
AI Agent(智能体) 是一种能够感知环境、自主决策、执行动作并完成复杂目标的 AI 系统。核心特征:
- 自主性:无需人类逐步指令,能自主规划任务步骤;
- 工具使用:通过 Function Calling / MCP 调用外部工具;
- 记忆能力:维护短期(会话上下文)和长期(知识库、用户画像)记忆;
- 反思能力:根据执行反馈自我修正。
Workflow(工作流):
- 预定义的确定性流程,节点和边由开发者硬编码(如 DAG),数据按固定路径流转。
- 每个节点的输入输出格式固定,无自主决策能力。
本质区别:
| 维度 | Workflow | AI Agent |
|---|---|---|
| 决策方式 | 人工预设规则 | LLM 自主决策 |
| 灵活性 | 低,分支逻辑需提前编码 | 高,动态选择工具和路径 |
| 可解释性 | 高,流程可追溯 | 中,需依赖观测日志 |
| 适用场景 | 标准化流程(审批、ETL) | 开放性问题(客服、研究助手) |
| 容错 | 需人工处理异常分支 | 可自主重试、切换工具 |
关系 :Workflow 和 Agent 并非互斥。复杂 Agent 内部可包含 Workflow 子任务(如 "生成报告" Agent 中,"数据收集→分析→排版" 可用 Workflow 保证稳定性),这种混合架构称为 Agentic Workflow。
70. 🔥 Agent 的设计范式:ReAct、Plan-and-Execute、Reflexion 的原理与适用场景?
答案:
ReAct(Reasoning + Acting):
- 原理:将推理(Thought)和行动(Action)交织进行。每步 LLM 先思考 "我需要做什么",然后执行工具调用,观察结果(Observation),再进入下一步思考。
- 流程:Thought → Action → Observation → Thought → ...
- 适用:工具调用链短、需要实时反馈调整的场景(如问答、简单任务执行)。
- 缺点:每步都需 LLM 参与,Token 消耗高;无法提前规划长链路。
Plan-and-Execute:
- 原理:先由 LLM 制定完整计划(Plan,分解为子任务列表),再按顺序执行每个子任务,最后汇总结果。
- 流程:Plan → Execute Step 1 → Execute Step 2 → ... → Synthesize
- 适用:任务明确、可预先分解的复杂任务(如 "制定北京 3 日游计划并预订酒店")。
- 优化:可引入 RePlan,执行中若某步失败,重新规划剩余步骤。
Reflexion(反思):
- 原理 :在 ReAct 基础上增加自我反思 层。Agent 执行任务后,评估结果是否成功,若失败则分析原因,将教训写入记忆(Memory),下次遇到类似问题时避免重复错误。
- 核心组件:Evaluator(评估结果)、Self-Reflection(生成反思文本)、Memory Store(存储经验)。
- 适用:需要持续学习、长期运行的 Agent(如代码生成、自动化测试)。
选型建议:
- 简单工具调用 → ReAct;
- 复杂多步任务 → Plan-and-Execute;
- 需持续优化 → Reflexion。
71. 🔥 Agent 的记忆机制如何设计?短期记忆、长期记忆、语义记忆分别怎么实现?
答案:
记忆是 Agent 突破上下文窗口限制、实现个性化和持续学习的关键。
短期记忆(Short-Term Memory,工作记忆):
- 作用:维护当前会话的上下文,支持多轮对话中的指代消解和话题连贯。
- 实现 :直接放入 LLM 的 Prompt 上下文窗口。窗口满时采用:
- 滑动窗口:保留最近 N 轮对话;
- 摘要压缩:对早期对话做 LLM 摘要,替换原始对话;
- Token 预算管理:为系统提示、历史、工具结果分配 Token 配额,超限时按优先级丢弃。
长期记忆(Long-Term Memory):
- 作用:跨会话记住用户信息、偏好、历史行为。
- 实现 :
- Profile 记忆:结构化存储用户属性(如 "用户偏好:Python、厌恶 Java"),存于数据库或 Redis。
- 事件记忆:记录关键事件("2026-04-01 用户购买了 iPhone 16"),按时间线存储。
- 总结记忆:定期对会话做摘要,提取关键事实存入长期记忆。
语义记忆(Semantic Memory):
- 作用:存储概念性知识,支持相似语义检索。
- 实现:通过 Embedding 模型将记忆文本向量化,存入向量数据库。当 Agent 需要回忆时,用当前 Query 做相似度检索,提取相关记忆片段注入 Prompt。
记忆检索策略:
- 最近性(Recency):优先使用最近的记忆;
- 相关性(Relevance):通过向量相似度匹配;
- 重要性(Importance):让 LLM 给记忆打分,高优先级保留。
- 综合评分:
score = α * recency + β * relevance + γ * importance。
72. 🔥 Multi-Agent 协作有哪些架构模式?Supervisor、Swarm、Hierarchical 的区别?
答案:
Multi-Agent 系统将复杂任务分解给多个专业化 Agent,通过协作提升整体能力和鲁棒性。
Supervisor(监督者模式):
- 结构:一个中心 Supervisor Agent + 多个 Worker Agent。
- 流程:Supervisor 接收任务,分析后分发给合适的 Worker;Worker 执行后返回结果;Supervisor 汇总、校验、输出最终答案。
- 适用:任务类型多样、需要统一调度的场景(如智能客服:Supervisor 分发给 "订单查询 Agent"、"售后 Agent"、"技术支持 Agent")。
- 优点 :集中控制,易于观测和干预;缺点:Supervisor 单点瓶颈,复杂任务分发逻辑重。
Swarm(蜂群模式):
- 结构:多个对等 Agent,无中心节点。
- 流程:Agent 间通过消息广播或点对点通信自主协商任务分配,类似分布式系统中的 P2P。
- 适用:需要高并发、去中心化的场景(如舆情监控:多个 Agent 分别监控不同平台,共享发现)。
- 优点 :无单点故障,扩展性强;缺点:协作逻辑复杂,易出现冲突和重复劳动。
Hierarchical(层级模式):
- 结构:树状层级,顶层 Manager 将任务分解给中层,中层再分解给底层执行 Agent。
- 适用:超复杂任务,需要多层抽象(如自动驾驶:顶层 "路径规划" → 中层 "车道保持" → 底层 "转向控制")。
- 优点 :责任清晰,适合模块化;缺点:层级过多导致延迟高,信息传递失真。
工程实践 :LangGraph 的 Supervisor 节点、AutoGen 的 GroupChat、OpenAI Swarm 框架都提供了这些模式的实现。
73. 🔥 LLM 网关需要解决哪些问题?多模型路由、限流熔断、成本治理如何实现?
答案:
LLM 网关是 AI 应用的核心基础设施,位于客户端与大模型供应商之间,解决生产环境的稳定性、成本和性能问题。
核心问题与方案:
1. 多模型路由(Model Routing):
- 按能力路由:简单任务 → 轻量模型(如 GPT-4o-mini),复杂任务 → 强模型(如 GPT-4o / Claude 3.5)。
- 按成本路由:非关键场景用便宜模型,关键场景用贵模型。
- 按负载路由:某模型供应商限流时,自动切换到备用供应商(Failover)。
- 实现:维护模型候选列表和评分卡,网关层根据请求特征(Prompt 长度、任务类型、用户等级)动态选择。
2. 限流与熔断:
- 限流:按用户、按应用、按模型维度限流。令牌桶算法控制 QPS,防止单用户打爆 API Key。
- 熔断:监控模型错误率、延迟、超时率,达到阈值后熔断(Open),返回降级结果或切换模型;定期 Half-Open 探测恢复。
- 并发控制:限制同时向某模型发送的请求数,避免触发供应商限流。
3. 成本治理:
- Token 计费:精确统计 Input / Output Token,按用户/项目分摊成本。
- 缓存:对重复或相似 Query,使用语义缓存(Semantic Cache,向量相似度匹配),命中则直接返回缓存结果,减少 LLM 调用。
- Prompt 压缩:使用 LLMLingua 等工具压缩 Prompt,减少 Input Token。
- 模型降级:高并发时,自动将部分请求降级到更便宜的模型。
4. 其他:
- 统一 API 适配:屏蔽不同厂商的 API 差异(OpenAI、Claude、文心、通义),对外暴露统一接口。
- 可观测性:记录每次调用的延迟、Token 数、成本、模型版本,用于优化和计费。
74. 🔥 流式输出(SSE / WebSocket)在 AI 应用中的选型与首包优化?
答案:
SSE(Server-Sent Events)vs WebSocket:
| 维度 | SSE | WebSocket |
|---|---|---|
| 协议 | 基于 HTTP,单向(服务端→客户端) | 基于 TCP,全双工 |
| 兼容性 | 好,支持 HTTP 基础设施(负载均衡、CDN) | 需专门支持,某些企业防火墙会拦截 |
| 复杂度 | 低,自动重连、事件 ID 追踪 | 高,需自己处理心跳、重连 |
| 适用场景 | 单向推送(AI 流式生成、股票行情) | 双向实时通信(游戏、协同编辑) |
AI 生成场景选型:
- 大模型文本生成:SSE 是首选。因为只需服务端单向推送 Token,客户端无需频繁发送数据;且 SSE 基于 HTTP,易接入现有网关、鉴权体系。
- 语音/视频实时交互:WebSocket 或 WebRTC(更低延迟)。
首包优化(Time To First Token, TTFT):
- 模型预热:保持模型常驻 GPU 显存,避免冷启动(Serverless 场景用预留实例)。
- Prompt 缓存:对重复的系统提示(System Prompt)做 Prefix Caching(如 vLLM 的 Automatic Prefix Caching),减少重复计算。
- 动态批处理(Dynamic Batching / Continuous Batching):推理框架(vLLM、TensorRT-LLM)将多个请求的解码阶段拼接成批次,提高 GPU 利用率,减少单个请求的排队等待。
- 首包探测:网关层在多个模型实例间选择响应最快的节点(如 Ragent 的 ProbeBufferingCallback)。
- 流式提前输出:部分场景可先返回固定话术("正在思考..."),再推送真实内容,改善用户感知。
75. 🔥 大模型上下文窗口有限,有哪些工程化方案处理超长上下文?
答案:
虽然 2026 年 Gemini 2.5 Pro 已支持 100 万 Token、Claude 支持 200K,但超长上下文仍面临成本高、注意力稀释、延迟大等问题。
工程化方案:
- RAG(检索增强) :
- 不将全部文档塞入上下文,而是检索最相关的片段。这是处理超长文档最经济有效的方式。
- 上下文压缩(Context Compression) :
- LLMLingua:用小模型(如 GPT-2)对 Prompt 做压缩,去除冗余词,保留关键信息,压缩率可达 20 倍。
- 选择性上下文:计算每个 Token 的信息熵,删除低信息量的句子。
- Map-Reduce :
- 将长文档切分为多个块,每块独立让 LLM 处理(Map),最后汇总结果(Reduce)。适合摘要、提取任务。
- 滑动窗口 + 摘要 :
- 对早期对话做 LLM 摘要,只保留摘要 + 最近原始对话。
- 分层索引 :
- 对超长文档(如 1000 页手册)先构建目录级摘要,用户提问时先匹配到章节,再检索该章节内的细节。
- 长文本专用模型 :
- 使用支持超长上下文的模型(Gemini 2.5 Pro、Kimi、Claude 3)处理必须全文的场景,但需承担更高成本。
76. 🔥 如何评估 RAG 系统的效果?RAGAS 指标有哪些?
答案:
RAG 系统不能只看"能跑",需要量化评估检索质量和生成质量。
RAGAS(Retrieval-Augmented Generation Assessment)指标:
- Faithfulness(忠实度) :生成内容是否被上下文支持。用 LLM 判断回答中的每个陈述是否能从
contexts中推断。值越高幻觉越少。 - Answer Relevancy(答案相关性):生成内容与问题的相关程度。通过 LLM 生成问题的潜在问题,计算与原始问题的相似度。
- Context Precision(上下文精确率):检索到的上下文中,与问题相关的块占比。值越高说明噪声越少。
- Context Recall(上下文召回率):回答问题所需的所有信息,有多少被成功检索到。需要人工标注或 LLM 判断 "ground truth" 所需信息是否在上下文中。
- Context Entity Recall:上下文中包含的实体占答案中实体的比例。
人工评估维度:
- 检索准确率:Top-K 中相关文档的比例;
- 回答完整性:是否覆盖问题的所有方面;
- 引用准确性:溯源引用是否正确对应原文。
评估实践:
- 构建 评测集(Golden Dataset):包含 Query、标准答案、相关文档标注;
- 定期用评测集跑批评估,监控指标漂移;
- A/B 测试:对比不同 Embedding 模型、分块策略、Rerank 模型的效果。
77. 🔥 设计一个电商智能客服 Agent,技术架构如何设计?
答案:
系统架构(分层设计):
1. 接入层:
- 多渠道接入:Web、App、微信小程序、钉钉。
- 负载均衡 + LLM 网关:统一限流、鉴权、多模型路由。
2. 意图识别层(Intent Classification):
- 用户输入先经过意图分类模型(轻量 BERT 或 LLM),识别意图:
- 售前咨询(商品推荐、比价)
- 订单查询(物流、退款进度)
- 售后服务(退换货、投诉)
- 技术支持(使用问题)
- 闲聊/转人工
- 不同意图路由到不同的子 Agent 或 Workflow。
3. 知识层(RAG + 结构化数据):
- 商品知识库:商品详情、参数、用户评价 → 向量数据库(Milvus)。
- 订单/物流数据:通过 MCP Server 或 Function Calling 查询 MySQL / ES,不走 RAG(精确数据用 SQL)。
- 售后政策:PDF 文档 → RAG 检索。
- FAQ:高频问题走缓存或规则匹配,减少 LLM 调用。
4. Agent 执行层:
- ReAct Loop:理解问题 → 检索知识 / 查询订单 → 观察结果 → 生成回答。
- 工具集 :
query_order(order_id):查订单状态;query_logistics(tracking_no):查物流;apply_refund(order_id, reason):发起退款;search_product(keyword):商品检索。
- 记忆:记住用户近期浏览商品、历史订单、偏好(长期记忆存 Redis + 向量库)。
5. 输出层:
- 流式 SSE 返回,首包 < 500ms;
- 答案附带引用来源(商品页链接、政策条款编号);
- 敏感操作(退款)需人工确认或二次验证。
6. 观测与兜底:
- 护栏:检测敏感词、幻觉、越权操作;
- 转人工:置信度低、用户要求、重复失败时无缝转接人工客服,携带完整会话上下文;
- 会话复盘:定期分析未解决问题,优化知识库和 Prompt。
78. 🔥 AI 应用中的安全与护栏(Guardrails)如何设计?
答案:
护栏是 AI 应用生产落地的底线,需从输入、处理、输出三层防护。
输入层(Input Guardrails):
- Prompt 注入检测:识别 "忽略之前指令"、"DAN 模式" 等越狱攻击,使用规则 + 分类模型拦截。
- 敏感信息过滤:检测用户输入中的身份证号、银行卡号,做脱敏或拒绝处理。
- 长度限制:防止超长 Prompt 导致 Token 爆炸和 DoS 攻击。
处理层(Processing Guardrails):
- 权限控制:Agent 执行工具前校验用户权限(如 "查询订单" 只能查自己的)。
- 工具调用校验:严格校验 LLM 输出的工具参数(类型、范围、SQL 注入检测)。
- 沙箱执行:代码执行、文件操作在 Docker 沙箱或 WASM 中运行,防止破坏宿主环境。
输出层(Output Guardrails):
- 内容安全:检测涉政、涉黄、暴力、歧视内容,使用阿里云内容安全、Azure Content Safety 或自研分类模型。
- 事实一致性校验:对医疗、法律、金融回答,用知识库校验事实准确性,低置信度时拒答或标注 "仅供参考"。
- 结构化约束 :强制输出 JSON Schema,防止格式错乱导致下游解析失败(如
response_format={"type": "json_object"})。
工程实现:
- NeMo Guardrails:NVIDIA 开源,支持对话流程控制、主题限制、事实核查。
- 自研规则引擎:基于正则、关键词、向量相似度的多层过滤。
- 人机协同:高风险操作(退款、用药建议)必须人工审核。
79. 🔥 大模型推理优化有哪些手段?KV Cache、量化、投机解码?
答案:
1. KV Cache(键值缓存):
- 原理:Transformer 自注意力计算中,历史 Token 的 Key 和 Value 矩阵在生成新 Token 时不变。首次计算后缓存 KV,后续只需计算新 Token 的 Q,与缓存的 K、V 做注意力,将二次复杂度降为线性。
- 优化 :
- PagedAttention(vLLM):将 KV Cache 分页管理(类似 OS 虚拟内存),减少显存碎片,支持更大 batch size;
- Prefix Caching:缓存系统提示和重复前缀的 KV,减少重复计算。
2. 量化(Quantization):
- 原理:将 FP16/BF16 权重转为 INT8/INT4,减少显存占用和带宽压力,提升吞吐。
- 方案 :
- PTQ(训练后量化):GPTQ、AWQ,无需重新训练;
- QAT(量化感知训练):精度更高但成本大;
- GGUF(llama.cpp):CPU 推理的 INT4 量化,适合边缘设备。
- 注意:量化可能损失精度,需评测业务指标是否可接受。
3. 投机解码(Speculative Decoding):
- 原理:小模型(Draft Model)快速生成多个候选 Token,大模型(Target Model)一次性并行验证,接受正确的,拒绝的重新生成。
- 效果:利用大模型批处理能力,将串行生成转为并行验证,速度提升 2-3 倍,且输出分布与原始大模型完全一致(无损加速)。
其他优化:
- Continuous Batching(vLLM):动态拼接不同请求的解码阶段,提高 GPU 利用率;
- 张量并行 / 流水线并行:多 GPU 分布式推理;
- FlashAttention:优化 Attention 计算的内存访问模式,减少 HBM 读写。
80. 🔥 当大模型 API 响应延迟超过 1 秒时,前端可以采取哪些优化策略保证用户体验?
答案:
1. 流式渲染(Streaming UI):
- 使用 SSE 接收流式 Token,收到即渲染,用户感知首字时间而非全文时间。配合打字机效果,降低等待焦虑。
2. 骨架屏与渐进式加载:
- 请求发出后立即展示骨架屏或 "AI 思考中..." 动画;
- 先返回大纲或标题,再逐步填充细节(Progressive Disclosure)。
3. 乐观 UI(Optimistic UI):
- 对用户可预测的操作,先展示预期结果,后台异步确认。如发送消息后先显示在聊天框,AI 回复到达后替换。
4. 请求级优化:
- 请求去重:连续相同 Query 取消前一个请求(AbortController);
- 防抖:输入停止 300ms 后再发送请求;
- 预请求:用户输入时预发送部分上下文,减少实际请求时的计算量。
5. 降级策略:
- 超时后切换到低延迟模型(如从 GPT-4 降到 GPT-3.5);
- 返回缓存的相似问题答案(语义缓存);
- 提供 "重试" 按钮和离线模式。
6. 交互设计:
- 显示进度条或 Token 生成速度("已生成 128 tokens");
- 允许用户中断生成(Stop 按钮),避免无效等待;
- 对复杂任务提供 "后台运行 + 通知" 模式。
结语:以上 80 题覆盖 Java 基础、并发、JVM、数据库、缓存、框架、消息队列、网络、操作系统、分布式及 AI 工程化十大板块。建议按 ⭐ 必背 → 🔥 高频 → 普通题的优先级复习,每道题不仅要背诵,更要结合项目经历、源码阅读和线上排查经验组织答案。