大厂面试高频八股

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 引入 sizeCtltransferIndex 等变量,扩容时支持多线程协助迁移transfer 方法),提升扩容效率。


3. 🔥 ArrayList 与 LinkedList 底层实现、扩容机制、适用场景?

答案

ArrayList :底层是 Object[] 数组。默认初始容量 10,扩容时增长为原来的 1.5 倍oldCapacity + (oldCapacity >> 1)),使用 Arrays.copyOf 复制数据。支持随机访问,查询 O(1),增删中间元素 O(n)。适合查询多、增删少的场景。

LinkedList :底层是双向链表Node<E> 包含 previtemnext)。无需扩容,增删只需修改指针 O(1),但查询需遍历 O(n)。适合频繁增删、队列/栈实现的场景。

工程注意

  • ArrayList 若已知数据量,应通过构造函数指定初始容量,避免多次扩容带来的数组拷贝开销。
  • LinkedList 内存占用更高(每个节点需维护两个指针),且缓存局部性差,现代 CPU 下遍历性能往往不如 ArrayList

4. ⭐ synchronized 底层原理,锁升级过程?

答案

synchronized 底层通过 Monitor(管程) 实现,依赖对象头的 Mark Word 存储锁状态。

锁升级过程(不可逆,除偏向锁可撤销)

  1. 无锁:对象刚创建,无竞争。
  2. 偏向锁 :单线程反复进入同步块,Mark Word 记录线程 ID。下次该线程进入只需 CAS 替换线程 ID,无需原子操作。JDK 15 后默认关闭(-XX:-UseBiasedLocking),因多核环境下竞争频繁,偏向锁撤销成本高。
  3. 轻量级锁 :出现竞争时,线程在栈帧创建 Lock Record,通过 CAS 将对象头指向 Lock Record。自旋等待(默认 10 次,自适应自旋根据历史调整)。
  4. 重量级锁 :自旋失败,线程阻塞,进入 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 约定

  • 若两个对象 equalstrue,则 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,分为 ErrorException

  • Error :严重系统级错误(如 OutOfMemoryErrorStackOverflowError),程序无法恢复,不应捕获。
  • Exception :程序可处理的异常,分为:
    • 受检异常(Checked Exception) :编译期强制处理(如 IOExceptionSQLException),需 try-catchthrows
    • 非受检异常(Unchecked Exception) :运行时异常(RuntimeException 及其子类,如 NullPointerExceptionIllegalArgumentException),编译期不强制处理。

工程实践

  • 业务异常应继承 RuntimeException,避免方法签名污染;
  • 捕获异常应精确,避免裸 catch (Exception e)
  • 异常信息应包含上下文(参数、业务单号),但生产环境日志中不得打印敏感信息。

8. 🔥 String、StringBuilder、StringBuffer 的区别?String 为什么不可变?

答案

特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全(只读) 不安全 安全(synchronized)
性能 低(频繁拼接产生大量对象) 中(同步开销)
适用场景 字符串常量 单线程拼接 多线程拼接

String 不可变的原因

  1. 字符串常量池复用:相同字符串共享引用,节省堆内存;若可变,常量池引用会被意外修改。
  2. HashCode 缓存StringhashCode 可缓存,作为 HashMap 键时性能极高。
  3. 线程安全:天然线程安全,无需同步。
  4. 安全 :网络连接、文件路径等以 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 大参数

  1. corePoolSize:核心线程数,即使空闲也保留(除非 allowCoreThreadTimeOut)。
  2. maximumPoolSize:最大线程数。
  3. keepAliveTime:非核心线程空闲存活时间。
  4. unit:时间单位。
  5. workQueue:任务等待队列(ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue)。
  6. threadFactory:线程工厂,可自定义线程名、守护状态。
  7. handler:拒绝策略。

线程池大小设置

  • CPU 密集型corePoolSize = CPU 核心数 + 1(+1 防止页缺失导致 CPU 空闲)。
  • IO 密集型corePoolSize = CPU 核心数 * 2 或更大,公式:线程数 = CPU 核数 * (1 + 平均等待时间 / 平均工作时间)

拒绝策略

  • AbortPolicy(默认):抛异常;
  • CallerRunsPolicy:由调用线程执行任务,起到流量控制作用;
  • DiscardPolicy:静默丢弃;
  • DiscardOldestPolicy:丢弃队列最老任务。

工程实践 :使用 ThreadPoolExecutor 手动创建,禁止 Executors 的快捷方法(FixedThreadPoolSingleThreadExecutor 允许无限队列,有 OOM 风险;CachedThreadPool 允许无限线程)。


11. ⭐ ThreadLocal 原理?内存泄漏问题?为什么 key 要用弱引用?

答案

原理 :每个 Thread 对象内部维护 ThreadLocalMapThreadLocal.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:增加布尔标记,用于标识节点是否被删除。

优缺点

  • 优点:无锁并发,避免线程切换开销,性能高。
  • 缺点
    1. ABA 问题;
    2. 自旋 CAS 长时间失败会浪费 CPU(可引入退避策略);
    3. 只能保证单个变量原子性,无法解决多变量复合操作的原子性问题。

13. 🔥 CountDownLatch、CyclicBarrier、Semaphore 的使用场景与原理?

答案

CountDownLatch(倒计时门闩)

  • 原理:基于 AQS 的共享模式,初始化计数器 state=NcountDown()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 内存模型规定所有变量存储在主内存,每个线程有自己的工作内存。线程对变量的操作必须在工作内存中进行,不能直接读写主内存。

内存交互操作lockunlockreadloaduseassignstorewriteread/loadstore/write 必须成对出现,但不保证连续执行。

happens-before 规则(无需同步即可保证有序性):

  1. 程序次序规则:同一个线程内,前面的操作 happens-before 后面的操作。
  2. 监视器锁规则unlock happens-before 后面对同一把锁的 lock
  3. volatile 规则volatile 写 happens-before 后面对该变量的读。
  4. 线程启动规则Thread.start() happens-before 线程内所有操作。
  5. 线程终止规则 :线程内所有操作 happens-before 线程终止检测(Thread.join() 返回)。
  6. 中断规则interrupt() happens-before 检测到中断事件。
  7. 对象终结规则 :构造函数执行 happens-before finalize()
  8. 传递性: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 条件)

  1. 互斥:资源一次只能被一个线程占用;
  2. 占有且等待:线程持有资源同时等待其他资源;
  3. 不可抢占:已分配的资源不能被强制剥夺;
  4. 循环等待:线程间形成资源等待环路。

排查方法

  • jstack -l <pid>:查看线程状态,搜索 "Found one Java-level deadlock"
  • jconsole / VisualVM:图形化检测死锁。
  • 日志中关注 BLOCKED 状态的线程堆栈。

避免策略

  1. 破坏占有且等待 :一次性申请所有资源(如 ReentrantLocktryLock 同时获取多把锁)。
  2. 破坏循环等待:资源按全局顺序申请(如先申请锁 A,再申请锁 B)。
  3. 设置超时tryLock(timeout),超时则释放已持有锁并重试。
  4. 使用无锁编程: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 等回调方法中抛异常会导致后续链路中断,应配合 exceptionallyhandle

三、JVM(6 道)

18. ⭐ JVM 内存模型(运行时数据区)?JDK 8 元空间替代永久代的原因?

答案

运行时数据区

  1. 程序计数器:线程私有,记录当前执行字节码行号,唯一无 OOM 区域。
  2. 虚拟机栈 :线程私有,存储栈帧(局部变量表、操作数栈、动态链接、方法返回地址)。StackOverflowError(深度过大)/ OutOfMemoryError(无法扩展)。
  3. 本地方法栈:为 Native 方法服务。
  4. :线程共享,存放对象实例和数组。分代:新生代(Eden、Survivor0、Survivor1)和老年代。
  5. 方法区(元空间 Metaspace):存储类信息、常量、静态变量、即时编译器编译后的代码。JDK 8 前为永久代(PermGen),后为元空间。

元空间替代永久代的原因

  1. OOM 风险 :永久代大小固定(-XX:MaxPermSize),动态生成类(如 CGLIB、反射、OSGi)容易溢出;元空间使用本地内存,默认只受物理内存限制。
  2. HotSpot 与 JRockit 融合:JRockit 无永久代,为统一虚拟机架构。
  3. 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,比初始标记长)→ 并发清除。
  • 缺点
    1. CPU 敏感:并发阶段占用 CPU 资源;
    2. 浮动垃圾:并发清理时产生的新垃圾需预留空间,否则触发 "Concurrent Mode Failure",退化为 Serial Old;
    3. 内存碎片:标记-清除算法导致,可能提前触发 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 的条件?

答案

对象晋升老年代

  1. 年龄阈值 :对象在 Survivor 区每熬过一次 Minor GC,年龄 +1,默认达到 15 岁-XX:MaxTenuringThreshold)晋升。
  2. 动态年龄判断: Survivor 区中相同年龄对象大小总和超过 Survivor 空间的一半,则大于等于该年龄的对象直接晋升。
  3. 大对象直接进入老年代-XX:PretenureSizeThreshold(仅 Serial 和 ParNew 有效),避免在 Eden 和 Survivor 间频繁拷贝。
  4. 空间分配担保:Minor GC 前,JVM 检查老年代最大连续空间是否大于新生代所有对象总空间。若不足,看是否允许担保失败;若不允许或历史上担保失败,则先 Full GC。

触发 Full GC 的条件

  1. 老年代空间不足;
  2. 元空间不足;
  3. System.gc() 调用(建议 JVM 参数 -XX:+DisableExplicitGC 禁用);
  4. CMS 的 Concurrent Mode Failure / Promotion Failure;
  5. 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 排查

  1. 堆 OOM
    • 现象:java.lang.OutOfMemoryError: Java heap space
    • 排查:jmap -dump:format=b,file=... <pid> 生成 dump,用 MAT / VisualVM 分析 dominator tree,查找大对象和引用链。
    • 解决:检查内存泄漏(静态集合持有对象未释放)、增大堆内存、优化对象生命周期。
  2. 元空间 OOM
    • 现象:OutOfMemoryError: Metaspace
    • 排查:检查动态生成类(CGLIB、反射、Groovy 脚本)是否未卸载。
    • 解决:增大 MaxMetaspaceSize,检查类加载器泄漏。
  3. 线程栈 OOM
    • 现象:Unable to create new native thread
    • 排查:线程数超过系统限制(ulimit -u)或内存不足。
    • 解决:减少线程池大小,检查是否有线程泄漏。

23. 🔥 类加载机制?双亲委派模型?如何打破双亲委派?

答案

类加载过程:加载 → 验证 → 准备 → 解析 → 初始化。

  • 加载 :通过全限定名获取二进制字节流,生成 Class 对象。
  • 验证:文件格式、元数据、字节码、符号引用验证。
  • 准备 :为类变量(static)分配内存并设零值。
  • 解析:符号引用转直接引用。
  • 初始化 :执行 <clinit>() 方法(静态变量赋值、静态代码块)。

双亲委派模型

  • 类加载器层次:Bootstrap(%JAVA_HOME%/lib)→ Extension(ext 目录)→ Application(classpath)→ 自定义。
  • 机制:收到加载请求后,先委派给父加载器,父加载器无法完成时才自己加载。
  • 好处 :防止核心类被篡改(如自定义 java.lang.String),避免重复加载。

打破双亲委派

  1. Tomcat :Web 应用隔离需求,不同 Web 应用可能依赖同一库的不同版本。Tomcat 为每个 Web 应用创建独立的 WebAppClassLoader,先尝试自己加载,失败再委派,实现类隔离。
  2. SPI 机制 :如 JDBC,DriverManager 由 BootstrapClassLoader 加载,但具体驱动实现由 AppClassLoader 加载。通过 Thread.currentThread().getContextClassLoader() 获取线程上下文类加载器(默认为 AppClassLoader)来加载实现类,破坏了双亲委派。
  3. OSGi:模块化热部署,每个 Bundle 有自己的类加载器,网状加载结构。

四、MySQL(8 道)

24. ⭐ MySQL 索引底层数据结构?为什么用 B+树?聚簇索引 vs 非聚簇索引?

答案

为什么用 B+树

  • B 树 vs B+树:B 树非叶子节点也存数据,导致节点能存储的键值少,树更高;B+树非叶子节点只存索引键,数据都在叶子节点,节点扇出更大,树更矮胖,IO 次数更少。
  • 顺序访问:B+树叶子节点通过双向链表连接,范围查询和排序只需顺序遍历叶子节点,效率高。
  • 稳定查询:所有查询路径长度相同(从根到叶子),性能稳定。
  • 哈希索引:仅支持等值查询,不支持范围查询和排序,且哈希冲突时性能下降。

聚簇索引 vs 非聚簇索引

  • 聚簇索引:数据行和索引存储在一起,叶子节点就是数据页。InnoDB 表必有聚簇索引(主键索引),若无主键则选第一个非空唯一索引,否则隐式创建 6 字节的 row_id。
  • 非聚簇索引(二级索引) :叶子节点存储主键值,查询时需回表(根据主键再去聚簇索引查数据)。
  • 覆盖索引:查询字段都在二级索引中,无需回表,性能极高。

25. ⭐ 索引失效场景?

答案

  1. 违反最左前缀法则 :联合索引 (a,b,c),查询条件缺少 a 或只有 b,c 时失效。
  2. 范围查询右侧列失效where a=1 and b>2 and c=3c 的索引失效(b 是范围查询,其后的列无法使用索引)。
  3. 索引列参与计算/函数where left(name, 3) = 'abc'where age + 1 = 18
  4. 隐式类型转换 :字段是 varchar,传入数字(where phone = 13800138000),MySQL 会隐式转换字段类型,导致索引失效。
  5. LIKE 以 % 开头where name like '%abc' 无法使用索引;like 'abc%' 可以。
  6. OR 条件:OR 两侧只要有一侧未使用索引,整体失效(MySQL 8.0 索引合并优化有所改善)。
  7. 不等于(!= / <>)和 NOT IN:通常不走索引(选择性极低时可能走)。
  8. 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_idm_ids 中最小值;
    • max_trx_id:下一个即将分配的事务 ID。

可见性判断 :遍历 Undo Log 链,找到第一个满足 DB_TRX_ID < min_trx_idDB_TRX_ID == creator_trx_idDB_TRX_ID > max_trx_idDB_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 UPDATELOCK IN SHARE MODE)通过临键锁解决。

29. 🔥 慢查询优化思路?EXPLAIN 分析关注哪些字段?

答案

优化思路

  1. 定位慢 SQL :开启慢查询日志(slow_query_log),设置阈值(long_query_time)。
  2. 分析执行计划 :使用 EXPLAINEXPLAIN ANALYZE(MySQL 8.0.18+)。
  3. 索引优化:添加缺失索引、优化联合索引顺序、消除索引失效。
  4. SQL 改写 :避免 SELECT *,减少回表;拆分大 SQL;用连接替代子查询。
  5. 架构优化:读写分离、分库分表、引入缓存。

EXPLAIN 关键字段

  • type :访问类型,从优到劣:system > const > eq_ref > ref > range > index > ALL。至少应达到 rangeALL 表示全表扫描。
  • key:实际使用的索引。
  • rows:预估扫描行数,越小越好。
  • Extra
    • Using index:覆盖索引,无需回表;
    • Using where:Server 层过滤;
    • Using filesort:需要额外排序,性能差;
    • Using temporary:使用临时表,常见于 GROUP BYDISTINCT
    • Using index condition:索引下推(ICP),在存储引擎层过滤数据,减少回表。

30. 🔥 分库分表方案?ShardingKey 如何选择?分页查询问题?

答案

拆分方式

  • 垂直拆分:按业务模块拆分(用户库、订单库),解决单库数据量过大、高并发压力,但会引入分布式事务和跨库 Join。
  • 水平拆分:按某种规则将同一表数据拆到多库多表(如 user_0 ~ user_7),解决单表数据量过大(> 5000 万行或 > 100GB)。

ShardingKey 选择原则

  1. 高频查询字段:如用户 ID、订单 ID,保证 80% 查询能直接定位到分片;
  2. 数据均匀:避免热点(如按时间分片导致近期分片压力过大);
  3. 避免跨片查询:常用方案:哈希取模(均匀但扩容麻烦)、一致性哈希(平滑扩容)、范围分片(易扩容但可能热点)。

分页查询问题

  • 分页需聚合多个分片的结果,再排序取页,深分页时性能极差(如 LIMIT 1000000, 10)。
  • 解决方案
    1. 禁止深分页:产品层限制最大页码;
    2. 游标分页 :使用上次查询的最大 ID 作为条件(where id > last_id limit 10);
    3. 二次查询法 :先查各分片 LIMIT offset, n,汇总排序后确定全局 offset,再回查具体数据;
    4. 全局索引表:将排序字段和 ID 建全局索引表,先查索引表再回查详情。

31. 🔥 主从复制原理?读写分离延迟问题如何解决?

答案

主从复制原理

  1. Master 将变更写入 Binlog(逻辑日志,有三种格式:Statement、Row、Mixed)。
  2. Slave 的 IO Thread 连接 Master,读取 Binlog 写入本地 Relay Log
  3. Slave 的 SQL Thread 重放 Relay Log,完成数据同步。

Binlog 格式

  • Statement :记录 SQL 原文,日志小,但某些函数(UUID()NOW())会导致主从不一致;
  • Row:记录每行变更前后数据,一致性高,日志大;
  • Mixed:默认 Statement,特殊场景自动切换 Row。

延迟问题解决方案

  1. 强制走主库:对一致性要求极高的查询(如刚修改后立刻查),通过 Hint 或框架层标记走 Master。
  2. 半同步复制rpl_semi_sync_master_enabled,至少一个 Slave 收到并写入 Relay Log 后才返回成功,降低延迟概率(但牺牲一定性能)。
  3. 并行复制 :MySQL 5.7+ 基于 Group Commit 的 slave_parallel_workers,按库或按事务组并行重放。
  4. 数据同步层:引入 Canal 监听 Binlog,将变更同步到 Redis/ES,读请求直接走缓存或搜索引擎。

五、Redis(6 道)

32. ⭐ Redis 五种基本数据类型的底层实现?

答案

  1. String :底层是 SDS(Simple Dynamic String) ,结构包含 lenallocflagsbuf[]。相比 C 字符串:O(1) 获取长度、二进制安全、预分配空间减少内存重分配、惰性释放。
  2. List :JDK 7 前是双向链表 + ziplist;JDK 7 后是 quicklist(双向链表 + ziplist 节点),兼顾插入删除和内存紧凑。
  3. Hash :数据量少时用 ziplist (紧凑列表),数据量多转为 hashtable(字典,两个 dictht,渐进式 rehash)。
  4. Set :整数元素且数量少时用 intset (有序整数数组),否则用 hashtable(值存为 key,value 为 NULL)。
  5. 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. ⭐ 缓存穿透、缓存击穿、缓存雪崩的原因与解决方案?

答案

缓存穿透:查询一个数据库和缓存都不存在的数据,导致每次请求都打到数据库。

  • 解决
    1. 布隆过滤器:在缓存前加布隆过滤器,快速判断 key 是否可能存在;
    2. 缓存空值:对不存在的 key 也缓存一个短时效的空值(注意占用内存和短期不一致)。

缓存击穿:热点 key 突然过期,大量请求同时打到数据库。

  • 解决
    1. 互斥锁:获取锁后查库并回写缓存,其他线程等待或降级;
    2. 逻辑过期:不设置 TTL,通过逻辑时间判断数据是否过期,过期时异步更新;
    3. 热点 key 预加载:定时续期,避免过期。

缓存雪崩:大量 key 同时过期,或 Redis 宕机,导致数据库压力骤增。

  • 解决
    1. 随机过期时间:在基础 TTL 上加随机偏移,避免集中过期;
    2. 多级缓存:本地缓存(Caffeine)+ Redis + 数据库;
    3. 熔断降级:数据库压力过大时,熔断部分请求返回默认值;
    4. 高可用: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)实例的锁,才算获取成功。
  • 争议
    1. 时钟漂移:若某 Redis 节点时钟超前,锁可能提前过期,导致两个客户端同时持有锁;
    2. 网络延迟:获取锁的耗时难以精确控制;
    3. 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不支持。

三级缓存

  1. singletonObjects(一级缓存):存放完全初始化好的 Bean(成品)。
  2. earlySingletonObjects(二级缓存):存放实例化但未赋值的 Bean(半成品),用于解决循环依赖。
  3. singletonFactories(三级缓存) :存放生成早期引用的 ObjectFactory,用于生成代理对象。

解决流程(A ↔ B)

  1. 创建 A,实例化后(未赋值)将 ObjectFactory 放入三级缓存
  2. A 属性赋值时发现需要 B,开始创建 B;
  3. B 实例化后放入三级缓存,属性赋值时发现需要 A;
  4. 从三级缓存获取 A 的 ObjectFactory,生成早期引用放入二级缓存,B 完成注入和初始化;
  5. B 放入一级缓存,A 继续完成属性赋值和初始化。

为什么需要三级缓存(而非二级)

  • 若 A 需要代理(AOP),早期引用必须是代理对象而非原始对象。三级缓存中的 ObjectFactory 可在需要时调用 BeanPostProcessor 生成代理对象,放入二级缓存。若只有二级缓存,无法区分是否需要提前创建代理。

构造器注入不行

  • 构造器调用时对象尚未实例化,无法提前暴露半成品。Spring 在调用构造器前没有任何对象引用可放入缓存,无法打破循环。

39. ⭐ Spring IoC 容器启动流程?Bean 的生命周期?

答案

IoC 容器启动流程

  1. 加载配置 :读取 XML / 注解 / JavaConfig,封装为 BeanDefinition
  2. BeanDefinition 注册 :存入 BeanDefinitionRegistryConcurrentHashMap)。
  3. BeanFactoryPostProcessor 执行 :如 PropertySourcesPlaceholderConfigurer 解析占位符,修改 BeanDefinition
  4. Bean 实例化 :调用构造器创建对象(createBeanInstance)。
  5. 属性赋值populateBean,注入依赖(DI)。
  6. 初始化initializeBean
    • Aware 接口回调(BeanNameAwareApplicationContextAware);
    • BeanPostProcessor.postProcessBeforeInitialization
    • @PostConstruct / InitializingBean.afterPropertiesSet() / 自定义 init-method;
    • BeanPostProcessor.postProcessAfterInitialization(AOP 代理在此生成)。
  7. 使用:Bean 就绪。
  8. 销毁 :容器关闭时,@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 失效场景

  1. 非 public 方法:Spring AOP 基于代理,只能拦截 public 方法;
  2. 同类内部调用this.method() 不走代理,事务不生效。应注入自身代理或拆分到另一个 Bean;
  3. 异常被吞掉 :默认只回滚 RuntimeExceptionError,若捕获异常未抛出,或抛出受检异常,事务不回滚;
  4. 异步方法@Async 需在事务外调用,否则事务可能未提交;
  5. 数据库引擎不支持:如 MyISAM 不支持事务。

41. 🔥 Spring Boot 自动装配原理?

答案

核心注解链
@SpringBootApplication@EnableAutoConfiguration@Import(AutoConfigurationImportSelector.class)

流程

  1. AutoConfigurationImportSelector 实现 DeferredImportSelector,在 BeanDefinition 加载阶段被调用。
  2. 读取所有 META-INF/spring.factories(Spring Boot 2.7+ 改为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)中配置的 EnableAutoConfiguration 类全限定名。
  3. 通过 SpringFactoriesLoader 加载候选配置类列表。
  4. 利用 条件注解 过滤:
    • @ConditionalOnClass:类路径存在某类时生效;
    • @ConditionalOnMissingBean:容器中没有该 Bean 时生效;
    • @ConditionalOnProperty:配置文件中某属性满足条件时生效。
  5. 符合条件的配置类被注册到容器,完成自动装配。

自定义 Starter

  • 创建自动配置类,用 @ConditionalOnClass 等控制条件;
  • META-INF/spring.factories 中注册;
  • 提供 spring-configuration-metadata.json 实现 IDE 配置提示。

42. 🔥 MyBatis 执行流程?#{} 与 ${} 区别?一级缓存与二级缓存?

答案

执行流程

  1. 读取配置文件(mybatis-config.xml / Spring Boot 的 application.yml),构建 SqlSessionFactory
  2. 通过 SqlSessionFactory 创建 SqlSession(非线程安全,每次请求新建);
  3. SqlSession 调用 Mapper 接口方法,通过 JDK 动态代理 生成 MapperProxy;
  4. MapperProxy 解析注解/XML,生成 MappedStatement
  5. Executor 执行 SQL,参数处理(ParameterHandler)、结果映射(ResultSetHandler);
  6. 若开启二级缓存,Executor 先查二级缓存,再查一级缓存,最后查数据库。

#{} vs ${}`

  • #{}:预编译参数占位符,MyBatis 将其替换为 ?,通过 PreparedStatement 设置参数,防 SQL 注入
  • ${}:字符串直接替换,用于动态表名、列名等场景,存在 SQL 注入风险,必须严格校验输入。

缓存

  • 一级缓存SqlSession 级别,默认开启。同一会话内多次相同查询命中缓存。SqlSession 关闭后清空。
  • 二级缓存Mapper Namespace 级别,需手动开启(<cache/>@CacheNamespace)。多个 SqlSession 共享,基于 TransactionalCacheManager,提交时才真正放入缓存。
  • 注意 :二级缓存实体类必须实现 Serializable,且分布式环境下建议使用 Redis 替代 MyBatis 二级缓存。

七、消息队列(5 道)

43. ⭐ Kafka 高吞吐原理?ISR 机制?

答案

高吞吐四大核心

  1. 顺序写磁盘:Kafka 消息追加到日志文件末尾,顺序写性能接近内存写(600MB/s),远超随机写(100KB/s)。
  2. 页缓存(Page Cache):数据先写入操作系统页缓存,由 OS 异步刷盘,减少 JVM GC 和用户态/内核态拷贝。
  3. 零拷贝(Zero-Copy) :消费者读取时,通过 sendfile 系统调用,数据直接从 Page Cache 发送到网卡,绕过应用缓冲区,减少 2 次 CPU 拷贝和 2 次上下文切换。
  4. 批量压缩 :生产者批量发送(batch.sizelinger.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 事务消息实现原理?顺序消息如何实现?

答案

事务消息(半消息机制)

  1. 发送半消息 :生产者发送事务消息到 Broker,此时消息对消费者不可见(存在 RMQ_SYS_TRANS_HALF_TOPIC)。
  2. 执行本地事务 :生产者回调 executeLocalTransaction,执行数据库操作等本地事务。
  3. 提交或回滚
    • 本地事务成功,发送 COMMIT,Broker 将消息从半消息队列移到目标 Topic,消费者可见;
    • 本地事务失败,发送 ROLLBACK,Broker 删除半消息。
  4. 事务回查 :若生产者未返回状态或宕机,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. 🔥 消息幂等性如何保证?消息积压如何处理?死信队列?

答案

幂等性保证

  1. 数据库唯一索引:如订单号 + 消息 ID 建唯一键,重复插入抛异常捕获。
  2. Redis SETNXSET message_id 1 EX 86400 NX,利用原子性和过期时间防重。
  3. 状态机幂等:如订单状态只能 待支付→已支付→已发货,重复消息不会改变状态。
  4. Token 机制:服务端预分配 Token,消费时校验并删除。

消息积压处理

  1. 紧急扩容:增加消费者实例(需保证消费者逻辑无状态)。
  2. 跳过非关键消息:若部分消息可丢弃,临时跳过堆积时间段的消息。
  3. 优化消费逻辑:批量消费、异步处理、减少数据库交互。
  4. 重置消费位点:极端情况下,将消费位点重置到最新,牺牲部分数据实时性(需业务可接受)。

死信队列(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 怎么解决?

答案

三次握手

  1. SYN=1, seq=x(客户端 → 服务端)
  2. SYN=1, ACK=1, seq=y, ack=x+1(服务端 → 客户端)
  3. ACK=1, seq=x+1, ack=y+1(客户端 → 服务端)

为什么不是两次

  • 防止历史重复连接初始化。若客户端第一个 SYN 延迟到达,服务端直接建立连接,但客户端已放弃该连接,导致服务端资源浪费。
  • 同步双方初始序列号(ISN),两次握手只能确认一方的收发能力。

四次挥手

  1. FIN=1, seq=u(主动方 → 被动方)
  2. ACK=1, seq=v, ack=u+1(被动方 → 主动方,半关闭)
  3. FIN=1, ACK=1, seq=w, ack=u+1(被动方 → 主动方,数据发完)
  4. ACK=1, seq=u+1, ack=w+1(主动方 → 被动方)

TIME_WAIT(2MSL)作用

  1. 保证最后一个 ACK 能被对方收到,若丢失可重传;
  2. 防止已失效的连接请求报文出现在本连接中(等待网络中残留报文消失)。

大量 CLOSE_WAIT

  • 原因:被动关闭方收到 FIN 后,应用层未调用 close()socket 未关闭,导致无法发送 FIN。
  • 解决:检查代码逻辑,确保异常分支也关闭连接;检查是否有阻塞 IO 导致应用无法响应。

49. ⭐ TCP 与 UDP 区别?TCP 如何保证可靠传输?

答案

维度 TCP UDP
连接 面向连接 无连接
可靠性 可靠 不可靠
有序性 有序 无序
拥塞控制
首部开销 20 字节 8 字节
适用场景 HTTP、文件传输 DNS、视频流、游戏

TCP 可靠传输机制

  1. 确认应答(ACK):接收方返回确认号,告知期望下一个字节序号。
  2. 超时重传:发送方启动定时器,超时未收到 ACK 则重传。时间通过 RTT 动态计算(Jacobson 算法)。
  3. 滑动窗口:实现流量控制,允许发送方连续发送多个段而不等待确认,提高吞吐量。
  4. 流量控制 :接收方通过 Window Size 字段告知发送方剩余缓冲区大小,防止发送过快。
  5. 拥塞控制
    • 慢启动: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)

  1. Client Hello:客户端发送支持的加密套件、随机数(Client Random);
  2. Server Hello:服务端返回选定加密套件、证书、随机数(Server Random);
  3. 客户端验证证书,生成 Pre-Master Secret,用公钥加密发送;
  4. 双方用 Client Random + Server Random + Pre-Master Secret 生成会话密钥(对称加密);
  5. Finished 消息,后续通信使用对称加密。

TLS 1.3:简化握手,仅需 1-RTT(甚至 0-RTT 恢复会话),废弃 RSA 密钥交换,仅支持 ECDHE,提升安全性和速度。


51. 🔥 输入 URL 到页面显示的全过程?

答案

  1. DNS 解析 :浏览器缓存 → OS 缓存(hosts)→ 本地 DNS 服务器 → 根域名服务器 → 顶级域名服务器 → 权威域名服务器,返回 IP 地址。可能走 DNS 预解析(<link rel="dns-prefetch">)。
  2. TCP 连接:三次握手建立连接。HTTPS 还需 TLS 握手。
  3. 发送 HTTP 请求:构建请求行、请求头、Cookie 等,经 TCP/IP 协议栈封装为数据包。
  4. 网络传输:经过路由器、交换机,可能穿越 NAT,到达服务端。中间经过 CDN 边缘节点时可能直接返回缓存。
  5. 服务端处理:Nginx 反向代理 → 网关(限流、鉴权)→ 应用服务器(Spring Boot)→ 业务逻辑 → 数据库/缓存查询 → 生成响应。
  6. 浏览器解析渲染
    • 解析 HTML 构建 DOM 树;
    • 解析 CSS 构建 CSSOM 树;
    • DOM + CSSOM → Render Tree;
    • Layout(回流):计算元素几何位置;
    • Paint(重绘):绘制像素;
    • Composite:合成图层,GPU 渲染。
  7. 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)。
  • 调度流程
    1. 新 Goroutine 优先放入当前 P 的 LRQ;
    2. M 绑定 P,从 LRQ 取 G 执行;
    3. LRQ 空时从全局队列(GRQ)或其他 P 偷取(Work Stealing);
    4. 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%MEMRES(实际物理内存)。按 1 显示各核负载,按 H 显示线程。
  • ps -ef | grep java / ps -eo pid,pcpu,pmem,comm --sort=-pcpu:查看进程详细信息。
  • netstat -tunlp / ss -tunlp :查看网络连接、监听端口、对应进程。ssnetstat 更快(从内核直接读取)。
  • 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 情况,关注 YGCYGCTFGCFGCTGCT

十、分布式系统 & 微服务(5 道)

56. ⭐ CAP 理论与 BASE 理论?分布式系统如何权衡 CP vs AP?

答案

CAP 理论:分布式系统无法同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance),最多满足两项。

  • 分区容错性 P :网络分区不可避免,必须满足。因此实际在 CPAP 间选择。

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 原理?时钟回拨问题如何解决?

答案

常见方案

  1. 数据库自增:简单,但单点瓶颈、性能低、暴露数据量。
  2. UUID:无序、字符串存储空间大、索引效率低。
  3. Redis 自增 :原子性 INCR,但依赖 Redis 可用性。
  4. Snowflake(雪花算法):主流方案。

Snowflake 结构(64 位):

  • 1 位符号位(0)+ 41 位时间戳(毫秒级,约 69 年)+ 10 位机器 ID(5 位数据中心 + 5 位机器)+ 12 位序列号(每毫秒 4096 个 ID)。
  • 趋势递增,Long 类型存储,索引友好。

时钟回拨问题

  • 服务器 NTP 同步或手动调整时间,导致当前时间小于上次生成 ID 的时间戳,可能生成重复 ID。
  • 解决方案
    1. 等待:时钟回拨幅度小(< 5ms)时,线程自旋等待;
    2. 异常抛出:回拨幅度大时抛异常,由上层重试或报警;
    3. 备用位:借用 1 位机器 ID 作为回拨标识,回拨时切换标识位;
    4. 美团 Leaf:采用 ZooKeeper 分配趋势递增的号段,不依赖机器时钟。

59. 🔥 限流算法:计数器、滑动窗口、令牌桶、漏桶?Sentinel 熔断降级原理?

答案

限流算法

  1. 计数器:固定窗口,单位时间内计数,超过则拒绝。实现简单,但窗口边界可能突增 2 倍流量(临界问题)。
  2. 滑动窗口:将窗口细分为多个子窗口,统计最近 N 个子窗口的请求数。平滑流量,但内存占用随子窗口数增加。
  3. 令牌桶 :以固定速率向桶中放令牌,请求需获取令牌才能通过。允许一定程度的突发流量(桶有余量时),Google Guava RateLimiter 使用此算法。
  4. 漏桶:请求进入漏桶,以固定速率流出。强制平滑流量,无法应对突发(适合流量整形)。

Sentinel 熔断降级

  • 状态机Closed(关闭)→ Open(打开)→ Half-Open(半开)。
  • Closed:正常放行,统计错误率 / 慢调用比例。
  • Open:错误率超过阈值,熔断打开,请求快速失败。经过熔断时长(如 5s)后进入 Half-Open。
  • Half-Open:放行少量请求探测,若成功则关闭熔断,失败则重新打开。
  • 降级策略:RT 慢调用比例、异常比例、异常数。

60. 🔥 一致性算法 Raft / Paxos 的基本原理?

答案

Raft(易懂,工程常用)

将一致性问题分解为三个子问题:

  1. Leader 选举:节点初始为 Follower,超时未收到心跳则变为 Candidate,发起投票,获得多数票成为 Leader。
  2. 日志复制:Leader 接收客户端请求,写入本地日志,并行发送给 Followers;多数确认后提交,应用到状态机,再返回客户端。
  3. 安全性 :通过任期(Term)和日志匹配保证:
    • 同一任期内最多一个 Leader;
    • 已提交的日志必须存在于新 Leader;
    • Follower 只接受任期 >= 当前任期的 Leader 日志。

Paxos(理论奠基)

  • 角色:Proposer(提议者)、Acceptor(接受者)、Learner(学习者)。
  • 两阶段:
    1. Prepare:Proposer 生成全局唯一编号 n,请求 Acceptor 承诺不再接受编号 < n 的提案;
    2. 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,检索增强生成) 是一种将外部知识检索与大模型生成结合的技术架构,解决大模型知识截止、幻觉和私有数据缺失问题。

完整链路(离线 + 在线)

离线阶段(知识入库)

  1. 文档解析:PDF/Word/网页等非结构化文档提取文本,需处理表格、图片 OCR(如 MinerU)、多栏排版。
  2. 文档切割(Chunking)
    • 固定长度切割(简单,但易截断语义);
    • 递归字符切割(按段落、句子层级);
    • 语义切割(基于 Embedding 相似度断点);
    • 父子块(Parent-Document Retrieval):小块用于检索,大块用于上下文生成。
  3. 向量化(Embedding):通过 Embedding 模型(BGE-M3、OpenAI text-embedding-3)将文本转为高维向量。
  4. 索引构建:写入向量数据库(Milvus、Qdrant、PGVector),构建 HNSW / IVFFLAT 等近似最近邻索引。

在线阶段(查询生成)

  1. Query 理解:意图识别、Query 改写(扩展、纠错、HyDE)。
  2. 检索召回:向量相似度搜索(Top-K)+ 稀疏检索(BM25、全文搜索)多路召回。
  3. 重排序(Rerank):使用 Cross-Encoder(BGE-Reranker)对候选文档精排序,提升相关性。
  4. 上下文组装:将 Top-N 文档按模板组装为 Prompt 上下文。
  5. 大模型生成:LLM 基于检索到的上下文生成回答,可配合引用溯源(Citation)。
  6. 后处理:答案脱敏、格式校验、幻觉检测。

62. 🔥 RAG 中如何解决文档切割导致的语义截断问题?

答案

问题本质:固定长度切割(如每 500 token)可能将连贯段落切断(如 "Java 内存模型包含..." 被切到两块),导致检索时语义不完整,生成质量下降。

解决方案

  1. 语义切割(Semantic Chunking)
    • 按句子/段落边界切割,保证每块语义完整;
    • 使用 Embedding 模型计算相邻句子的相似度,相似度低于阈值时作为切割点。
  2. 递归字符切割(RecursiveCharacterTextSplitter)
    • 按优先级分割符递归切割:段落 → 句子 → 单词 → 字符,优先保持大粒度语义单元。
  3. 父子块(Parent-Document Retrieval)
    • 子块(小粒度,如 256 token)用于 Embedding 和检索,保证检索精度;
    • 父块(大粒度,如 2048 token,包含子块所在完整段落)用于生成上下文,保证语义完整。
    • 检索命中子块后,回查其父块作为 LLM 输入。
  4. 重叠切割(Overlap)
    • 相邻块保留一定重叠区域(如 50 token),减少边界信息丢失。
  5. 结构化切割
    • 对 Markdown、HTML、JSON 等结构化文档,按标题层级(H1/H2/H3)切割,保持文档结构。

63. 🔥 什么是 Query 改写?HyDE 和 Step-Back Prompting 的原理?

答案

Query 改写(Query Rewriting / Expansion):用户原始查询往往简短、歧义、口语化,直接用于向量检索效果差。Query 改写通过 LLM 或规则将原始 Query 转换为更适合检索的形式。

常见策略

  1. Query 扩展:补充同义词、相关术语。如用户问 "JVM 调优",扩展为 "JVM 调优 GC 参数 OOM 排查"。
  2. HyDE(Hypothetical Document Embeddings,假设文档嵌入)
    • 原理:让 LLM 根据用户 Query 生成一篇假设的理想答案文档,对该文档做 Embedding,再用这个向量去检索真实文档。
    • 优势:将 Query 从短文本空间映射到文档空间,解决 Query 与文档的语义鸿沟(如用户问 "怎么解决死锁",生成的假设文档包含 "jstack 排查、避免循环等待" 等关键词,检索更精准)。
    • 代价:增加一次 LLM 调用,延迟和成本上升。
  3. Step-Back Prompting
    • 原理:让 LLM 先退一步,从具体 Query 抽象出高层概念或原理,再基于原理检索。
    • 示例:用户问 "我的 Redis 缓存雪崩了怎么办" → 抽象为 "缓存雪崩的成因与解决方案" → 检索通用解决方案文档,而非仅匹配 "我的 Redis" 这种噪声。
    • 适用:技术问答、医疗问诊等需要原理推导的场景。

64. 🔥 RAG 的多路召回方案有哪些?如何做 Rerank?

答案

多路召回(Multi-Channel Retrieval):单一检索方式存在局限,多路召回通过多种策略互补,提升召回率和准确率。

常见召回路

  1. 向量召回(Dense Retrieval):基于 Embedding 语义相似度,适合同义改写、概念匹配。使用 HNSW 近似最近邻搜索,召回 Top-K。
  2. 稀疏召回(Sparse Retrieval):基于 BM25、TF-IDF、倒排索引的关键词匹配,适合专有名词、ID、精确术语。
  3. 知识图谱召回:对实体关系型查询(如 "张三的直属领导是谁"),通过图数据库(Neo4j)直接查询关系。
  4. 全文检索:Elasticsearch 分词匹配,适合长文档中的关键词命中。
  5. 历史会话召回:从对话历史中提取相关上下文,解决指代消解(如 "它有什么缺点" 中的 "它")。

召回结果融合

  • 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 过度发挥或上下文过长,仍会产生幻觉。

解决策略

  1. 检索质量提升
    • 优化 Query 改写和 Embedding 模型,确保召回文档高相关;
    • 使用 Rerank 精排,过滤低质量文档;
    • 设置相关性阈值,低于阈值的文档不送入 LLM。
  2. Prompt 工程约束
    • 明确指令:"请仅基于以下上下文回答问题,若上下文不包含答案,请回答 '我不知道',不要编造"。
    • Few-shot 示例:给出生成和拒答的示例。
  3. 引用溯源(Citation)
    • 要求 LLM 在回答中标注引用来源(如 [1] [2]),用户可追溯到原文验证。
    • 实现:在 Prompt 中要求 "每句话后标注来源编号",后处理校验引用编号是否在提供的文档中。
  4. 答案一致性校验
    • Self-RAG:让 LLM 生成答案后,再判断答案是否被上下文支持(Generate → Reflect → Retrieve)。
    • 多模型验证:用另一个小模型做事实核查(Fact Checking)。
  5. 护栏(Guardrails)
    • 使用 NeMo Guardrails 或自研规则,对输出做敏感信息、事实一致性、格式合规校验,拦截幻觉回答。

66. ⭐ Function Calling 的原理是什么?Tool Schema 如何设计?

答案

Function Calling(工具调用) 是大模型与外部系统交互的核心机制,让 LLM 能够"使用工具"完成计算、查询、操作等自身不擅长的任务。

原理

  1. 开发者在请求中传入 Tools 定义(JSON Schema 描述可用函数)。
  2. LLM 根据用户 Query 和 Tools 描述,自主决策是否需要调用工具、调用哪个工具、传入什么参数。
  3. LLM 返回特殊的 function_call / tool_calls 消息(而非直接回答)。
  4. 应用层接收到调用指令后,执行对应函数,获取结果。
  5. 将函数执行结果再次送入 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_ordercreate_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)及通信流程?

答案

三层架构

  1. MCP Host:承载 AI 应用的环境(如 Claude Desktop、Cursor、自研 Agent 平台)。一个 Host 可包含多个 Client。
  2. MCP Client:与 Server 建立 1:1 连接,负责协议握手、能力协商、消息路由。遵循 JSON-RPC 2.0 规范。
  3. MCP Server:独立进程或服务,封装特定领域能力(如 PostgreSQL、Slack、文件系统)。通过标准接口暴露资源(Resources)、工具(Tools)和提示(Prompts)。

通信流程

  1. 初始化 :Client 发送 initialize 请求,协商协议版本、能力(如是否支持 Streaming)。
  2. 工具发现 :Client 发送 tools/list,Server 返回可用工具列表及其 JSON Schema。
  3. 调用执行
    • LLM 决定调用工具后,Client 发送 tools/call,携带工具名和参数;
    • Server 校验参数 Schema,执行本地逻辑(如查询数据库);
    • Server 返回执行结果(文本、图片、错误信息)。
  4. 资源订阅 :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)

  1. 模型预热:保持模型常驻 GPU 显存,避免冷启动(Serverless 场景用预留实例)。
  2. Prompt 缓存:对重复的系统提示(System Prompt)做 Prefix Caching(如 vLLM 的 Automatic Prefix Caching),减少重复计算。
  3. 动态批处理(Dynamic Batching / Continuous Batching):推理框架(vLLM、TensorRT-LLM)将多个请求的解码阶段拼接成批次,提高 GPU 利用率,减少单个请求的排队等待。
  4. 首包探测:网关层在多个模型实例间选择响应最快的节点(如 Ragent 的 ProbeBufferingCallback)。
  5. 流式提前输出:部分场景可先返回固定话术("正在思考..."),再推送真实内容,改善用户感知。

75. 🔥 大模型上下文窗口有限,有哪些工程化方案处理超长上下文?

答案

虽然 2026 年 Gemini 2.5 Pro 已支持 100 万 Token、Claude 支持 200K,但超长上下文仍面临成本高、注意力稀释、延迟大等问题。

工程化方案

  1. RAG(检索增强)
    • 不将全部文档塞入上下文,而是检索最相关的片段。这是处理超长文档最经济有效的方式。
  2. 上下文压缩(Context Compression)
    • LLMLingua:用小模型(如 GPT-2)对 Prompt 做压缩,去除冗余词,保留关键信息,压缩率可达 20 倍。
    • 选择性上下文:计算每个 Token 的信息熵,删除低信息量的句子。
  3. Map-Reduce
    • 将长文档切分为多个块,每块独立让 LLM 处理(Map),最后汇总结果(Reduce)。适合摘要、提取任务。
  4. 滑动窗口 + 摘要
    • 对早期对话做 LLM 摘要,只保留摘要 + 最近原始对话。
  5. 分层索引
    • 对超长文档(如 1000 页手册)先构建目录级摘要,用户提问时先匹配到章节,再检索该章节内的细节。
  6. 长文本专用模型
    • 使用支持超长上下文的模型(Gemini 2.5 Pro、Kimi、Claude 3)处理必须全文的场景,但需承担更高成本。

76. 🔥 如何评估 RAG 系统的效果?RAGAS 指标有哪些?

答案

RAG 系统不能只看"能跑",需要量化评估检索质量和生成质量。

RAGAS(Retrieval-Augmented Generation Assessment)指标

  1. Faithfulness(忠实度) :生成内容是否被上下文支持。用 LLM 判断回答中的每个陈述是否能从 contexts 中推断。值越高幻觉越少。
  2. Answer Relevancy(答案相关性):生成内容与问题的相关程度。通过 LLM 生成问题的潜在问题,计算与原始问题的相似度。
  3. Context Precision(上下文精确率):检索到的上下文中,与问题相关的块占比。值越高说明噪声越少。
  4. Context Recall(上下文召回率):回答问题所需的所有信息,有多少被成功检索到。需要人工标注或 LLM 判断 "ground truth" 所需信息是否在上下文中。
  5. 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)

  1. Prompt 注入检测:识别 "忽略之前指令"、"DAN 模式" 等越狱攻击,使用规则 + 分类模型拦截。
  2. 敏感信息过滤:检测用户输入中的身份证号、银行卡号,做脱敏或拒绝处理。
  3. 长度限制:防止超长 Prompt 导致 Token 爆炸和 DoS 攻击。

处理层(Processing Guardrails)

  1. 权限控制:Agent 执行工具前校验用户权限(如 "查询订单" 只能查自己的)。
  2. 工具调用校验:严格校验 LLM 输出的工具参数(类型、范围、SQL 注入检测)。
  3. 沙箱执行:代码执行、文件操作在 Docker 沙箱或 WASM 中运行,防止破坏宿主环境。

输出层(Output Guardrails)

  1. 内容安全:检测涉政、涉黄、暴力、歧视内容,使用阿里云内容安全、Azure Content Safety 或自研分类模型。
  2. 事实一致性校验:对医疗、法律、金融回答,用知识库校验事实准确性,低置信度时拒答或标注 "仅供参考"。
  3. 结构化约束 :强制输出 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 工程化十大板块。建议按 ⭐ 必背 → 🔥 高频 → 普通题的优先级复习,每道题不仅要背诵,更要结合项目经历、源码阅读和线上排查经验组织答案。

相关推荐
yoyo_zzm2 小时前
Laravel6.x新特性全解析
java·spring boot·后端
Nick_zcy2 小时前
小说在线阅读网站和小说管理系统 · 功能全解析
java·后端·python·springboot·ruoyi
源码宝2 小时前
基于 SpringBoot + Vue 的医院随访系统:技术架构与功能实现
java·vue.js·spring boot·架构·源码·随访系统·随访管理
qinqinzhang2 小时前
Java 中的 IoC、AOP、MVC
java
禾叙_3 小时前
【langchain4j】结构化输出(六)
java·开发语言
饭小猿人3 小时前
Android 腾讯X5WebView如何禁止系统自带剪切板和自定义剪切板视图
android·java
Advancer-3 小时前
第二次蓝桥杯总结(上)
java·算法·职场和发展·蓝桥杯
\xin3 小时前
pikachu自编SQL(POST)
java·数据库·sql
一行代码一行诗++3 小时前
注释是什么和注释该怎么写(C语言)
java·前端·javascript