序言

这份手册按技术方向整理了 25 个分类、共 358 道 Java 面试题,其中 98 道高频题给出了答题要点与核心解析。

它想解决三个很具体的问题:

  1. 不知道会被问什么。 面试题的范围看起来很宽,但真正高频的就那些。先按分类扫一遍题目,心里就有底了。
  2. 知道题目但答不利索。 每道精选题都按「考察点 → 核心答案 → 深度解析」组织,先看面试官想考什么,再看怎么答,最后补原理。
  3. 复习时找不到重点。 每个分类的题目按由浅入深排列,时间紧就看标了详解的那几道,时间充裕再顺着题目清单逐个过。

怎么用这份手册:

你的处境 建议读法
明天就面试 只看每节的「高频题详解」,按分类过一遍答题框架
还有一周 先扫全部题目清单,标出不会的,再针对性看解析
长期准备 按分类顺序读,每读完一节自己做一次口述复述

分类概览

分类 题目数 详解
Java 基础面试题 21 5
Java 集合面试题 14 4
MySQL 面试题 24 5
Redis 面试题 26 5
RocketMQ 面试题 8 3
MyBatis 面试题 13 4
Spring Cloud 面试题 11 4
Java 并发面试题 31 5
设计模式面试题 12 4
Spring 面试题 22 5
Tomcat 面试题 5 3
Zookeeper 面试题 9 3
RabbitMQ 面试题 9 3
Kafka 面试题 8 3
JVM 面试题 26 5
计算机网络面试题 14 4
操作系统面试题 21 5
Dubbo 面试题 12 4
RAG 面试题 17 4
AI Agent 面试题 12 4
Netty 面试题 10 4
分库分表面试题 14 4
高并发面试题 8 3
高可用面试题 2 2
Elasticsearch 面试题 9 3
合计 358 98

题目按由浅入深排列。标了详解的是最高频、也最容易答砸的那几道。


一、Java 基础面试题

共 21 题。

题目清单

  1. String 为什么设计成 final 不可变的?
  2. String、StringBuilder 和 StringBuffer 的区别?
  3. String str = new String(“abc”) 创建了几个对象?
  4. 接口和抽象类的区别是什么?怎么选择?
  5. while(true) 和 for(;;) 哪个性能更好?
  6. 什么是反射机制?为什么反射慢?
  7. 什么是泛型?为什么要使用它?
  8. 如何理解 Java 中的多态?
  9. 为什么 Java 不支持多继承?
  10. final、finally、finalize 的区别?
  11. finally 中代码一定会被执行吗?
  12. BigDecimal 和 Long 哪个表示金额更合适,怎么选择?
  13. AIO、BIO 和 NIO 的区别?
  14. 什么是 SPI,和 API 有啥区别?
  15. 为什么不能用浮点数表示金额?
  16. 深拷贝和浅拷贝的区别?
  17. 什么是序列化与反序列化?
  18. 为什么重写 equals 时一定要重写 hashCode?
  19. 什么是 UUID,能保证唯一性吗?
  20. Java 中异常分哪两类,有什么区别?
  21. 为什么不能用 BigDecimal 的 equals 方法做等值比较?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. String 为什么设计成 final 不可变的?

面试考察点

  1. 基础原理理解:面试官不仅仅是想知道 String 是不可变的,更是想考察你是否理解 Java 设计者的深层考量,以及不可变对象的设计思想。
  2. 多线程安全意识:考察你是否意识到不可变性是实现线程安全最简单、最可靠的方式之一。
  3. 性能优化思维:考察你是否了解字符串常量池、哈希缓存等 JVM 底层优化机制。
  4. 安全性认知:考察你对系统安全的敏感度,理解不可变性在类加载、敏感信息保护等方面的重要性。

核心答案

String 被设计成 final 不可变类,主要基于 5 大核心原因:

设计原因 核心价值 实际效果
字符串常量池优化 内存复用 相同字符串只存一份,节省堆内存
线程安全 无锁并发 不可变对象天生线程安全,无需同步
哈希值缓存 性能提升 hashCode() 只需计算一次,后续直接复用
安全性保障 系统稳定 防止类加载、文件路径等被篡改
设计一致性 行为可预测 子类无法破坏父类契约

一句话总结:不可变性是 String 实现高性能、高安全、高并发的基础保障。

深度解析

一、final 关键字如何保证不可变?

String 类通过 final 关键字从三个维度保证不可变性:

  1. 第一层(类级别):public final class String 声明类为 final,彻底杜绝继承。如果允许继承,恶意子类可能重写方法,将可变行为引入原本不可变的 String 体系,破坏所有依赖不可变性的代码。
  2. 第二层(字段级别):JDK 8 及之前使用 private final char[] value,JDK 9 改为 private final byte[] value(Compact Strings 优化)。final 修饰确保数组引用一旦赋值就永远指向同一个数组对象。
  3. 第三层(访问控制):private 修饰符配合没有任何 setter 方法的设计,外部代码既不能直接访问 value 数组,也无法通过方法修改其内容。所有看似”修改”的操作(如 substring()、concat())实际上都是创建新对象。

源码验证(JDK 8):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

// 核心存储:final 修饰,引用不可变
private final char value[];

// 缓存哈希值:懒加载,计算一次后永久缓存
private int hash; // Default to 0

// 没有 setter 方法!
// 所有修改操作都返回新对象
public String substring(int beginIndex) {
// 返回新 String 对象,原对象不变
return new String(value, beginIndex, subLen);
}
}

2. String、StringBuilder 和 StringBuffer 的区别?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道这三者的区别,更是想确认你是否理解 Java 字符串的不可变性设计,以及为什么需要 StringBuilder 和 StringBuffer。
  2. 线程安全意识:考察你是否清楚 StringBuilder 和 StringBuffer 在线程安全上的差异,能否根据业务场景选择合适的类。
  3. 性能优化意识:是否了解字符串拼接在不同场景下的性能差异,能否写出高性能的字符串处理代码。

核心答案

对比项 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全(不可变) 不安全 安全(synchronized)
性能 拼接差 最快 较快
适用场景 少量字符串、常量 单线程拼接 多线程拼接
出现版本 JDK 1.0 JDK 1.5 JDK 1.0

一句话总结:单线程用 StringBuilder,多线程用 StringBuffer,常量用 String。

深度解析

一、String 的不可变性

String 是 Java 中最常用的类之一,它的核心特性是 不可变(Immutable)。

  • 底层存储:String 内部使用 final char[] value(JDK 9 之后改为 byte[])存储字符数据,final 修饰意味着引用不可变。

  • 任何”修改”操作都会创建新对象:如拼接、截取、大小写转换等,原对象不变,返回新对象。

  • 不可变的好处:

    • 线程安全:多个线程可以安全共享,无需同步
    • 字符串常量池优化:相同字符串只存一份
    • 安全性:作为参数传递时不会被修改,适合作为 HashMap 的 key
二、StringBuilder 与 StringBuffer 的可变性

这两个类都继承自 AbstractStringBuilder,底层是 可扩容的字符数组。

  • 直接修改内部数组:append()、insert()、delete() 等方法直接操作原数组,不创建新对象。

  • 自动扩容:当容量不足时,自动扩容为原来的 2 倍 + 2,并将原数据复制到新数组。

  • 预分配容量:如果能预估最终长度,建议构造时指定容量,避免多次扩容:

    1
    2
    // 推荐:预估容量,避免扩容
    StringBuilder sb = new StringBuilder(1024);
三、线程安全性对比

3. String str = new String(“abc”) 创建了几个对象?

面试考察点

  1. 基础概念掌握:面试官不仅仅是想让你回答一个数字,更是想考察你是否理解 Java 对象创建机制、字符串常量池的工作原理,以及字面量与 new 关键字的区别。
  2. 分情况分析能力:这道题的答案取决于字符串常量池的当前状态,考察你是否有 “条件思维”,能否识别出不同的执行场景。
  3. JVM 内存模型理解:考察你是否清楚堆内存、字符串常量池的位置关系(JDK 6 vs JDK 7+ 的差异),以及对象在内存中的存储方式。

核心答案

这道题的答案 **取决于字符串常量池中是否已存在 "abc"**:

场景 常量池状态 创建对象数 对象位置
首次执行 不存在 "abc" 2 个 常量池 1 个 + 堆 1 个
非首次执行 已存在 "abc" 1 个 仅堆中 1 个

一句话总结:new String("abc") 至少创建 1 个堆对象,最多创建 2 个对象(取决于常量池状态)。

深度解析

一、执行过程详解
  • 步骤 1(类加载阶段):JVM 首先解析字符串字面量 "abc",检查字符串常量池。如果常量池中不存在 "abc",则创建一个 String 对象放入池中(对象①);如果已存在,则直接复用池中对象。
  • 步骤 2(运行阶段):new 关键字触发堆内存分配,创建一个新的 String 对象(对象②)。这个对象的 value 数组引用会指向常量池中 "abc" 对应的字符数据。
  • 步骤 3(赋值):变量 str 指向堆中新创建的 String 对象,而不是常量池中的对象。
二、内存布局图解
  • 对象①(常量池中):由字符串字面量 "abc" 触发创建,存储在字符串常量池中。这个对象可以被多个引用共享复用。
  • 对象②(堆中):由 new String() 显式创建,存储在普通堆区。这个对象是 str 变量独占的,不会被共享。
  • 数据共享:虽然创建了两个 String 对象,但它们内部可能共享同一份字符数组数据(取决于 JDK 版本和具体实现),这就是为什么 str.equals("abc") 返回 true。
三、代码验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StringObjectTest {
public static void main(String[] args) {
// 情况 1:首次执行,常量池无 "abc"
String s1 = new String("abc");

// 情况 2:非首次执行,常量池已有 "abc"
String s2 = new String("abc");

// 验证:s1 和 s2 是不同对象(都在堆中)
System.out.println(s1 == s2); // false

// 验证:s1 和常量池中的 "abc" 是不同对象
System.out.println(s1 == "abc"); // false

// 验证:字面量创建指向常量池
String s3 = "abc";
System.out.println(s3 == "abc"); // true

// 验证:内容相同
System.out.println(s1.equals(s3)); // true
}
}

4. 接口和抽象类的区别是什么?怎么选择?

面试考察点

  1. 基础概念掌握:面试官不仅仅是想知道你会不会定义接口和抽象类,更是想知道你是否理解它们在设计层面的本质区别—— “契约” 与 “模板” 的不同定位。
  2. 设计能力考察:通过这个问题,面试官想评估你的面向对象设计思维,能否根据实际场景做出合理的抽象选择,而不是滥用继承或接口。
  3. 版本演进意识:JDK 8 之后接口引入了默认方法和静态方法,这让接口和抽象类的界限变得模糊,考察你是否跟上 Java 的演进,以及在新特性下的选择逻辑。

核心答案

一句话总结:接口定义 “能做什么”(行为契约),抽象类定义 “是什么”(本质模板)。

核心区别对比
对比维度 接口 抽象类
设计理念 行为契约,定义 “能做什么” 模板模式,定义 “是什么”
继承关系 implements,可多实现 extends,单继承
成员变量 只能是 public static final 常量 任意访问修饰符,可定义普通成员变量
方法实现 JDK 8 前:只能抽象方法 JDK 8+:可有 default、static 方法 JDK 9+:可有 private 方法 可有抽象方法和具体实现
构造方法 ❌ 没有 ✅ 有(供子类调用)
访问修饰符 默认 public 任意(public、protected、默认)
选择口诀
  • 多实现用接口,单继承用抽象;
  • 行为规范用接口,代码复选用抽象;
  • 跨层次用接口,同家族用抽象。

深度解析

一、从设计层面理解本质区别

接口和抽象类最大的区别在于 设计意图,用一个生活化的例子来理解:

  • 接口(Flyable):定义的是 “飞行” 这个 能力/行为。飞机、鸟、超人都可以飞行,但它们本质完全不同,没有共同的父类。接口强调的是 “能做什么”。
  • 抽象类(Bird):定义的是 “鸟” 这个 本质/家族。麻雀、老鹰、企鹅都是鸟,它们有共同的特征(羽毛、翅膀、产卵等)和可以复用的代码。抽象类强调的是 “是什么”。

关键点:企鹅虽然不会飞(不实现 Flyable),但它依然是鸟(继承 Bird)。这就是 “本质” 和 “能力” 的区别。

二、JDK 版本演进带来的变化
  • JDK 7 及之前:接口是纯粹的抽象契约,只能包含抽象方法和常量,没有任何实现代码。
  • JDK 8:引入 default 方法和 static 方法,允许接口提供默认实现。这是为了支持 API 演进,比如 Collection 接口新增 forEach()、removeIf() 等方法,已有的 ArrayList、HashSet 等实现类无需修改就能获得新功能。
  • **JDK 9+**:进一步引入 private 方法,用于复用 default 方法中的公共代码逻辑,避免代码重复。

5. while(true) 和 for(;;) 哪个性能更好?

面试考察点

  1. 编译原理理解:面试官想知道你是否了解 Java 编译器如何处理不同语法形式的代码,以及它们在字节码层面是否存在差异。
  2. 性能优化意识:考察你是否具备从底层角度分析代码性能的思维,而不是停留在语法层面做无谓的纠结。
  3. 工程实践认知:验证你是否能区分 “理论差异” 和 “实际影响”,是否了解现代编译器的优化能力。

核心答案

性能完全相同,没有任何区别。

对比项 while(true) for(;;)
字节码 完全一致 完全一致
执行效率 相同 相同
JVM 优化 相同 相同
可读性 ✅ 更直观 需要适应

结论:两者经过 javac 编译后生成的字节码完全相同,运行时性能零差异。选择哪个完全取决于团队编码风格和个人习惯。

深度解析

一、字节码验证:编译后完全一致

我们用实际代码来验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class InfiniteLoopTest {

// while(true) 版本
public void whileLoop() {
while (true) {
System.out.println("while");
}
}

// for(;;) 版本
public void forLoop() {
for (;;) {
System.out.println("for");
}
}
}

使用 javap -c InfiniteLoopTest.class 查看字节码:

while(true) 的字节码:

1
2
3
4
5
6
public void whileLoop();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String while
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: goto 0 // 跳转回第 0 行

for(;;) 的字节码:

1
2
3
4
5
6
public void forLoop();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String for
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: goto 0 // 跳转回第 0 行

关键发现:两者都只使用一条 goto 指令实现循环跳转,结构完全相同。

二、编译器如何处理无限循环

二、Java 集合面试题

共 14 题。

题目清单

  1. ArrayList、LinkedList 与 Vector 的区别?
  2. HashMap、Hashtable 和 ConcurrentHashMap 的区别?
  3. Java中的集合类有哪些?如何分类的?
  4. 说说 HashMap 的扩容机制?如何扩容的?
  5. 为什么 HashMap 的默认负载因子要设置成 0.75?
  6. 为什么在 Jdk 8 中 HashMap 要转成红黑树?
  7. HashMap 用在并发场景中会出现什么问题?
  8. HashMap 的 remove 方法是如何实现的?
  9. HashMap 在 get 和 put 时,底层流程是怎样的?
  10. HashMap 的 hash 方法是如何实现的?
  11. Set 如何保证元素不重复的?
  12. 什么是 fail-fast?什么是 fail-safe?
  13. ConcurrentHashMap 是如何保证线程安全的?
  14. ConcurrentHashMap 为什么在 JDK 1.8 中废弃分段锁?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. ArrayList、LinkedList 与 Vector 的区别?

面试考察点

  1. 集合框架基础:面试官不仅仅想知道你能说出这三者的区别,更想考察你是否理解 List 接口的不同实现方式及其底层原理,包括数组与链表的本质差异。
  2. 性能敏感度:考察你是否清楚不同场景下的性能表现,能否根据实际业务需求(查询多还是增删多)选择合适的数据结构。
  3. 线程安全意识:Vector 作为线程安全的集合,面试官想了解你是否知道它的实现方式以及为什么在实际开发中很少使用。

核心答案

三者都实现了 List 接口,但底层实现和特性差异明显:

特性 ArrayList LinkedList Vector
底层结构 动态数组 双向链表 动态数组
线程安全 ❌ 不安全 ❌ 不安全 ✅ 安全(synchronized)
默认容量 10 无(链表无容量概念) 10
扩容机制 1.5 倍 无需扩容 2 倍
随机访问 O(1) 快 O(n) 慢 O(1) 快
头部插入/删除 O(n) 慢 O(1) 快 O(n) 慢
内存占用 连续内存,较少 节点额外存储前后指针 连续内存,较少
适用场景 查询多、尾部增删 频繁增删、尤其是头部 基本不用(性能差)

一句话总结:日常开发 90% 用 ArrayList,频繁头部增删用 LinkedList,Vector 基本被淘汰(可用 Collections.synchronizedList 或 CopyOnWriteArrayList 替代)。

深度解析

一、底层数据结构对比
  • ArrayList:使用连续的数组存储元素,每个元素通过下标直接定位。由于内存连续,CPU 缓存命中率高,遍历性能好。但插入删除需要移动后续元素。
  • LinkedList:每个元素包装成 Node 节点,包含数据、前驱指针 prev 和后继指针 next。节点分散在堆内存各处,插入删除只需修改指针,但随机访问需要从头遍历。
二、性能对比详解

1. 随机访问性能

1
2
3
4
5
6
7
// ArrayList - O(1)
ArrayList<String> arrayList = new ArrayList<>();
arrayList.get(1000); // 直接通过下标访问:elementData[index]

// LinkedList - O(n)
LinkedList<String> linkedList = new LinkedList<>();
linkedList.get(1000); // 需要从头或尾遍历到第 1000 个节点

ArrayList 的 get(int index) 直接返回 elementData[index],时间复杂度 O(1)。

LinkedList 需要判断 index 在前半部分还是后半部分,然后从 head 或 tail 开始遍历:

2. HashMap、Hashtable 和 ConcurrentHashMap 的区别?

面试考察点

  1. 线程安全认知:面试官不仅仅是想知道这三者谁是线程安全的,更是想考察你是否理解不同线程安全实现方式的性能差异,以及为什么 Hashtable 已经被淘汰。
  2. 并发编程深度:考察你是否了解 ConcurrentHashMap 的底层实现原理,包括 JDK 7 的分段锁和 JDK 8 的 CAS + synchronized 两种方案的演进。
  3. 生产实践能力:能否根据实际场景(读多写少、高并发等)选择合适的 Map 实现,避免盲目使用导致的性能问题或线程安全隐患。

核心答案

三者都是 Map 接口的实现类,但线程安全性和性能差异显著:

特性 HashMap Hashtable ConcurrentHashMap
线程安全 ❌ 不安全 ✅ 安全 ✅ 安全
锁粒度 无锁 整表锁(粗粒度) 桶级别锁(细粒度)
null 键/值 ✅ 都允许 ❌ 都不允许 ❌ 都不允许
初始容量 16 11 16
扩容倍数 2 倍 2n + 1 2 倍
迭代器 快速失败 快速失败 弱一致性
底层结构(JDK 8) 数组 + 链表/红黑树 数组 + 链表 数组 + 链表/红黑树
性能 最高(单线程) 最低(全表锁) 高(并发优秀)
适用场景 单线程或外部同步 已淘汰 高并发多线程

一句话总结:单线程用 HashMap,多线程用 ConcurrentHashMap,Hashtable 已被淘汰(历史遗留类)。

深度解析

一、线程安全实现对比
  • Hashtable:使用 synchronized 修饰几乎所有方法,一把大锁锁住整个哈希表。任何时刻只允许一个线程操作,即使是访问不同位置的元素也会相互阻塞,并发性能极差。
  • **ConcurrentHashMap (JDK 8)**:采用桶级别细粒度锁,每个桶(数组位置)独立加锁。线程 A 操作索引 2 的元素,线程 B 操作索引 5 的元素,两者互不影响,可并发执行。
二、ConcurrentHashMap 的演进

JDK 7:分段锁(Segment)

1
2
3
4
5
6
7
8
// JDK 7 的分段锁设计
public class ConcurrentHashMap<K, V> {
final Segment<K,V>[] segments; // 默认 16 个分段

static final class Segment<K,V> extends ReentrantLock {
transient volatile HashEntry<K,V>[] table; // 每个分段维护一个小哈希表
}
}

分段锁将整个 Map 分成多个段(默认 16 个),每段一把 ReentrantLock。并发度最高为分段数。

JDK 8:CAS + synchronized

3. Java中的集合类有哪些?如何分类的?

面试考察点

  1. 体系结构认知:面试官不仅仅想知道你能背出几个集合类名,更想考察你是否理解 Java 集合框架的整体设计思想,能否从接口到实现有一个清晰的层次认知。
  2. 选型能力:考察你是否了解不同集合类的特性差异,能否根据实际场景(有序、唯一、排序、线程安全等)选择合适的实现类。
  3. 深度与广度:这道题是 “开门题”,面试官通常会根据你的回答深入追问某个具体集合的原理,比如 HashMap 的扩容机制、ConcurrentHashMap 的线程安全实现等。

核心答案

Java 集合框架主要分为 两大体系:Collection 和 Map。

  • Collection 体系:存储单列元素,根据特性细分为 List(有序可重复)、Set(无序唯一)、Queue(队列)三大接口。
  • Map 体系:存储键值对(Key-Value),通过 Key 快速查找 Value,Key 必须唯一。

深度解析

一、Collection 体系详解

1. List 接口 - 有序可重复

实现类 底层结构 线程安全 特点 适用场景
ArrayList 动态数组 ❌ 查询 O(1),尾部增删 O(1) 大多数场景首选
LinkedList 双向链表 ❌ 头部增删 O(1),查询 O(n) 频繁头部增删
Vector 动态数组 ✅ 全表 synchronized 锁 已淘汰
Stack 动态数组 ✅ 继承 Vector,后进先出 已淘汰,用 ArrayDeque

2. Set 接口 - 无序唯一

实现类 底层结构 有序性 特点
HashSet 哈希表 ❌ 无序 性能最高,基于 HashMap
LinkedHashSet 哈希表 + 链表 ✅ 插入有序 保持插入顺序
TreeSet 红黑树 ✅ 排序有序 支持自然排序或自定义排序

3. Queue 接口 - 队列

实现类 类型 特点
LinkedList 双端队列 可作为队列、栈、列表使用
ArrayDeque 双端队列 循环数组实现,性能优于 LinkedList
PriorityQueue 优先队列 按优先级出队,非线程安全
ArrayBlockingQueue 阻塞队列 有界,线程安全,生产者消费者模型
LinkedBlockingQueue 阻塞队列 可选有界/无界,线程安全
二、Map 体系详解

4. 说说 HashMap 的扩容机制?如何扩容的?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道 “会扩容” 这个事实,更是想知道你是否理解扩容的触发条件、扩容阈值计算、以及扩容时元素如何重新分布。
  2. 源码理解深度:考察你是否读过 resize() 方法的源码,是否理解 JDK 1.8 中 “高低位链表” 的优化设计,以及为什么扩容后元素要么在原位置,要么在 “原位置 + 原容量” 的位置。
  3. 版本演进意识:JDK 1.7 和 JDK 1.8 的扩容机制有显著差异(头插法 vs 尾插法),考察你是否了解这些变化以及背后的原因(死循环问题)。

核心答案

HashMap 的扩容机制可以概括为:当元素数量超过 容量 × 负载因子 时,容量翻倍,并重新分配所有元素。

关键点 说明
默认初始容量 16
默认负载因子 0.75
扩容阈值 capacity × loadFactor(首次为 12 = 16 × 0.75)
扩容方式 容量翻倍(newCap = oldCap << 1)
触发条件 size > threshold 且当前桶位置非空
元素重分布 根据新增的最高位判断:原位置 或 原位置 + 原容量

深度解析

一、扩容触发时机

扩容发生在调用 putVal() 方法插入元素后,满足以下两个条件时触发:

  • 为什么需要两个条件? 这是一个性能优化。如果当前桶为空,直接放入即可,不需要立即扩容。只有当发生哈希冲突时,才考虑扩容以减少冲突概率。

  • threshold 的计算:threshold = capacity × loadFactor。默认情况下,首次扩容阈值为 12(16 × 0.75)。

  • 负载因子为什么是 0.75? 这是时间和空间的权衡:

    • 太大(如 1.0):空间利用率高,但哈希冲突多,查询效率低
    • 太小(如 0.5):哈希冲突少,查询效率高,但空间浪费
    • 0.75 是经过数学计算得出的平衡值,来自泊松分布
二、扩容核心流程
  • 步骤一 - 计算新容量:使用位运算 oldCap << 1 实现翻倍,效率极高。容量始终保持 2 的幂次方,这是为了让 (n - 1) & hash 能够均匀分布。
  • 步骤二 - 计算新阈值:新阈值也需要翻倍,保持负载因子的约束。
  • 步骤三 - 创建新数组:在堆中分配一个新的 Node 数组,大小为新容量。
  • 步骤四 - 迁移元素:这是最核心的步骤,需要遍历旧数组的每个桶,将链表或红黑树中的元素重新分配到新数组中。
  • 步骤五 - 更新引用:将 table 指向新数组,完成扩容。
三、元素重分布原理(核心!)

这是面试中最重要的考点。JDK 1.8 中,扩容后元素的位置分布有一个巧妙的规律:


三、MySQL 面试题

共 24 题。

题目清单

  1. char 和 varchar 的区别?
  2. InnoDB 和 MyISAM 有什么区别?
  3. MySQL 深度分页如何优化?
  4. 什么是回表查询?如何避免?
  5. 自增主键用完了怎么办?
  6. 为什么不推荐使用外键?
  7. 唯一索引和主键索引的区别?
  8. count(1)、count(*) 与 count(列名) 的区别?
  9. InnoDB 中索引类型有哪些?
  10. truncate、delete、drop 的区别?
  11. 慢查询怎么排查?
  12. 脏读、幻读、不可重复读是什么?
  13. 什么是数据库死锁,怎么解决?
  14. 用了索引还是很慢,可能是什么原因?
  15. 如何进行 SQL 调优?
  16. binlog、redolog 和 undolog 的区别?
  17. 设计索引时应遵循哪些原则?
  18. 为什么不建议使用存储过程?
  19. 为什么互联网大厂不推荐使用多表 JOIN?
  20. 什么是数据库范式,为什么要反范式化设计?
  21. 什么是最左前缀匹配?为什么要遵守?
  22. 乐观锁与悲观锁怎么实现?
  23. 谈谈 InnoDB 中的表级锁、页级锁、行级锁?
  24. InnoDB 加索引,这个时候会锁表吗?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. char 和 varchar 的区别?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道两个类型的字面区别,更是想知道你是否理解 MySQL 的存储机制,能否在实际表设计中做出正确选择。
  2. 性能意识:考察你是否清楚定长和变长类型在存储效率、查询性能、索引构建等方面的差异,以及在不同场景下的取舍。
  3. 实践经验:能否结合实际业务场景(如用户名、手机号、MD5 值等)给出合理的选择建议,而不是只会背概念。

核心答案

CHAR 和 VARCHAR 是 MySQL 中两种最常用的字符串类型,核心区别如下:

对比维度 CHAR VARCHAR
存储方式 定长存储,不足补空格 变长存储,按实际长度
长度范围 0 ~ 255 字符 0 ~ 65,535 字节
空间占用 固定占用声明长度 实际长度 + 1~2 字节前缀
尾部空格 存储时补齐,检索时移除 原样存储和检索
性能特点 更快的 CRUD(无长度计算) 更省空间(按需分配)
适用场景 长度固定的数据(手机号、MD5) 长度变化的数据(用户名、地址)

一句话总结:CHAR 是定长、空间换时间;VARCHAR 是变长、时间换空间。

深度解析

一、存储机制对比
  • CHAR 定长机制:无论实际存储多少字符,都占用声明长度的空间。比如 CHAR(10) 存储 'abc' 时,会自动补 7 个空格,占用 10 字节。检索时,尾部的空格会被自动移除(这是一个容易踩坑的点)。

  • VARCHAR 变长机制:只占用实际需要的空间,额外使用 1~2 字节存储长度信息。当长度 ≤ 255 字节时用 1 字节,超过 255 字节时用 2 字节。

  • 空间计算示例:

    • CHAR(10) 存储 'abc' → 占用 10 字节
    • VARCHAR(10) 存储 'abc' → 占用 4 字节(1 长度 + 3 数据)
    • VARCHAR(1000) 存储 'abc' → 占用 5 字节(2 长度 + 3 数据)
二、尾部空格处理差异(高频考点)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- CHAR 尾部空格会被移除
CREATE TABLE test_char (
name CHAR(10)
);

INSERT INTO test_char VALUES ('Tom '); -- 存入 'Tom' + 7 空格
SELECT name, LENGTH(name) FROM test_char;
-- 结果:'Tom', 3 (检索时空格被移除了!)

-- VARCHAR 尾部空格保留
CREATE TABLE test_varchar (
name VARCHAR(10)
);

INSERT INTO test_varchar VALUES ('Tom '); -- 存入 'Tom' + 7 空格
SELECT name, LENGTH(name) FROM test_varchar;
-- 结果:'Tom ', 10 (原样保留)

关键差异:

2. InnoDB 和 MyISAM 有什么区别?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道两个引擎的字面区别,更是想知道你是否理解 MySQL 存储引擎的底层机制,能否在实际项目中做出正确的技术选型。
  2. 架构设计能力:考察你是否清楚不同引擎在事务、锁、索引、崩溃恢复等方面的差异,能否根据业务特点(读多写少、高并发、强一致性等)选择合适的引擎。
  3. 生产实践经验:能否结合实际场景(如电商订单、日志分析、全文搜索等)给出合理的选择建议,而不是只会背概念。

核心答案

InnoDB 和 MyISAM 是 MySQL 最经典的两种存储引擎,核心区别如下:

对比维度 InnoDB MyISAM
事务支持 ✅ 支持 ACID 事务 ❌ 不支持事务
锁粒度 行级锁,支持 MVCC 表级锁
外键约束 ✅ 支持 ❌ 不支持
崩溃恢复 ✅ 支持(redo log、undo log) ❌ 不支持,可能数据损坏
聚簇索引 ✅ 聚簇索引(主键即数据) ❌ 非聚簇索引
全文索引 MySQL 5.6+ 支持 ✅ 原生支持
存储文件 .frm + .ibd .frm + .MYD + .MYI
表最大行数 受表空间限制(64TB) 受数据文件大小限制
COUNT(*) 性能 需要扫描全表 直接读取计数器
适用场景 事务处理、高并发写 只读、读多写少、全文搜索

一句话总结:InnoDB 是现代 MySQL 的默认引擎,支持事务和行锁,适合高并发 OLTP 场景;MyISAM 适合读多写少的分析型场景。

深度解析

一、事务与崩溃恢复
  • Redo Log(重做日志):采用 WAL(Write-Ahead Logging)机制,先写日志再写磁盘。即使数据库崩溃,也可以通过 redo log 恢复已提交的事务。这是 InnoDB 保证 持久性(Durability) 的核心机制。
  • Undo Log(回滚日志):用于事务回滚和 MVCC 多版本并发控制。当事务需要回滚时,通过 undo log 将数据恢复到修改前的状态;读取数据时,可以根据 undo log 看到数据的历史版本。
  • Buffer Pool:InnoDB 的内存缓冲池,数据和索引页都会先缓存到这里。修改操作先在内存中完成,再异步刷盘。
  • MyISAM 的致命缺陷:没有任何事务日志机制,写入过程中如果宕机,数据文件可能损坏且无法恢复。这是 MyISAM 逐渐被淘汰的核心原因之一。
二、锁机制对比
  • MyISAM 表级锁:

    • 读操作加读锁(共享锁),允许其他会话读取,但阻塞写操作
    • 写操作加写锁(排他锁),阻塞所有其他读写操作
    • 即使操作的是不同的行,也会互相阻塞
    • 适合读多写少的场景,因为读锁之间不冲突
  • InnoDB 行级锁:

3. MySQL 深度分页如何优化?

面试考察点

  1. 问题识别能力:面试官不仅仅是想知道优化方案,更是想看你能否识别出深度分页的性能瓶颈——为什么 LIMIT 1000000, 10 会慢?MySQL 到底在做什么?
  2. 原理理解深度:考察你是否理解 MySQL 的 LIMIT offset, size 执行机制——它需要扫描 offset + size 条记录再丢弃前面的,而不是”直接跳到第 offset 条”。
  3. 方案选型思维:深度分页有多种优化方案,考察你能否根据业务场景(是否允许用户跳页、是否有连续自增主键等)选择最合适的方案。

核心答案

深度分页问题:当使用 LIMIT offset, size 且 offset 值很大时,MySQL 需要扫描 offset + size 条记录后丢弃前面的 offset 条,性能急剧下降。

优化方案对比:

方案 核心思路 适用场景 性能提升
子查询优化 先查主键 ID,再回表查数据 有自增主键索引 ⭐⭐⭐⭐
INNER JOIN 同上,写法不同 有自增主键索引 ⭐⭐⭐⭐
游标分页 记录上次最大 ID,下次从该 ID 开始 连续翻页、不支持跳页 ⭐⭐⭐⭐⭐
BETWEEN 范围查询 用 ID 范围替代 LIMIT offset ID 连续、无断层 ⭐⭐⭐⭐⭐
业务限制 限制最大页数,禁止深度跳页 搜索引擎类场景 ⭐⭐⭐

一句话总结:深度分页的本质是 offset 导致的全表扫描,优化核心是 **”利用索引直接定位起始点,避免扫描后丢弃”**。

深度解析

一、问题本质:为什么深度分页会慢?

先看一个典型的深度分页 SQL:

1
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

很多人的误解:MySQL 会”直接跳到第 100 万条记录”,然后取 10 条。

实际情况:MySQL 的执行流程是:

  • 扫描量大:MySQL 需要扫描 1000000 + 10 = 1000010 条记录,而不是只扫描 10 条
  • 丢弃浪费:前 100 万条记录全部被扫描后丢弃,做了无用功
  • 回表开销:如果查询的是 SELECT *,每条记录都要回表获取完整数据,代价更大
  • 时间复杂度:O(offset + size),offset 越大,性能越差

这就是为什么 LIMIT 100, 10 很快,但 LIMIT 1000000, 10 会慢几个数量级。

二、方案一:子查询优化

核心思想:先通过子查询利用覆盖索引快速定位起始 ID,再回表查数据,避免大量无意义的回表操作。

1
2
3
4
5
6
7
8
-- 原始慢查询(假设执行时间:5 秒)
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

-- 优化后(假设执行时间:0.1 秒)
SELECT * FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) t ON o.id = t.id;

4. 什么是回表查询?如何避免?

面试考察点

  1. 索引原理理解:面试官不仅仅是想知道 “回表” 这个概念,更是想考察你是否理解 InnoDB 的聚簇索引和二级索引的区别,以及 B+ 树的存储结构。
  2. 性能优化意识:回表会带来额外的 I/O 开销,考察你是否具备 “减少回表次数” 的优化思维,能否在实际开发中设计出高效的索引。
  3. 覆盖索引应用:考察你是否掌握 “覆盖索引” 这一核心优化手段,以及如何通过 EXPLAIN 命令判断是否发生了回表。

核心答案

回表查询:当通过二级索引(非主键索引)查询数据时,如果 SELECT 的字段不完全包含在索引中,MySQL 需要先从二级索引树查到主键 ID,再回到聚簇索引树根据 ID 查找完整记录,这个过程叫 “回表”。

核心对比:

查询类型 索引类型 是否回表 性能
主键查询 聚簇索引 ❌ 不需要 ⭐⭐⭐⭐⭐ 最快
覆盖索引查询 二级索引(字段全覆盖) ❌ 不需要 ⭐⭐⭐⭐ 快
普通二级索引查询 二级索引(字段未覆盖) ✅ 需要回表 ⭐⭐⭐ 较慢

一句话总结:回表的本质是 “二级索引 → 聚簇索引” 的二次查找,通过 覆盖索引 可以避免。

深度解析

一、InnoDB 索引结构:理解回表的前提

要理解回表,首先要理解 InnoDB 的两种索引结构:

  • 聚簇索引(主键索引):叶子节点存储的是 完整的行数据,通过主键可以直接获取所有字段
  • 二级索引(非主键索引):叶子节点只存储 索引列的值 + 主键 ID,不包含其他字段

这就是为什么二级索引查询可能需要 “回表” —— 因为它没有完整的行数据!

二、回表过程演示

假设有一张用户表:

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE user (
id INT PRIMARY KEY, -- 主键
name VARCHAR(50), -- 姓名
age INT, -- 年龄
INDEX idx_name (name) -- name 列的二级索引
);

-- 插入测试数据
INSERT INTO user VALUES (1, 'Alice', 25);
INSERT INTO user VALUES (2, 'Bob', 30);
INSERT INTO user VALUES (3, 'Carol', 28);

场景:通过 name 查询完整数据

1
SELECT * FROM user WHERE name = 'Bob';

这个查询会发生回表,执行过程如下:

  • 步骤一:在二级索引 idx_name 中查找 name = 'Bob',找到对应的主键 id = 3
  • 步骤二:拿着主键 id = 3,回到聚簇索引树中查找完整的行数据
  • 回表代价:需要扫描两棵 B+ 树,产生额外的 I/O 开销
三、如何避免回表?—— 覆盖索引

覆盖索引(Covering Index):如果查询的所有字段都包含在索引中,MySQL 就不需要回表,直接从索引树获取数据即可。

优化前(会回表):

5. 自增主键用完了怎么办?

面试考察点

  1. 数据类型理解:面试官不仅仅是想知道 “用完了怎么办”,更是想考察你是否清楚不同整数类型的取值范围,以及 INT 和 BIGINT 的区别。
  2. 系统设计思维:考察你是否具备前瞻性的系统设计能力,能否在表设计阶段就考虑到数据量增长的问题。
  3. 分布式 ID 方案:当单机自增主键不够用时,是否了解分布式 ID 生成方案(雪花算法、UUID 等),这是高并发系统的必备知识。

核心答案

自增主键用完的后果:当自增 ID 达到数据类型的最大值后,再次插入数据会报错 Duplicate entry 'xxx' for key 'PRIMARY',新的数据无法写入。

各整数类型的主键容量:

数据类型 字节数 最大值 容量级
TINYINT 1 127(有符号)/ 255(无符号) 百级
SMALLINT 2 32767 / 65535 万级
MEDIUMINT 3 8388607 / 16777215 百万级
INT 4 2147483647 / 4294967295 21 亿 / 42 亿
BIGINT 8 9223372036854775807 / … 922 亿亿

一句话总结:主键用完会导致插入失败,解决方案有 改用更大的类型(BIGINT) 或 使用分布式 ID(雪花算法)。

深度解析

一、自增主键用完会发生什么?

当自增 ID 达到上限后,MySQL 的行为如下:

  • 自增到最大值:当 AUTO_INCREMENT 达到数据类型的上限时,无法继续递增
  • 插入失败:MySQL 尝试使用最大值插入,但该值已存在,导致主键冲突
  • 删除不回退:即使删除了中间的记录,AUTO_INCREMENT 值也不会变小
  • 影响范围:所有依赖自增主键的插入操作都会失败
二、解决方案一:改用更大的数据类型

最简单直接的方案:将 INT 改为 BIGINT。

1
2
3
4
5
6
7
8
-- 修改前:INT 最大约 21 亿(有符号)或 42 亿(无符号)
CREATE TABLE orders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32)
);

-- 修改后:BIGINT 最大约 922 亿亿
ALTER TABLE orders MODIFY id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY;

BIGINT 有多大?

注意事项:

  • 修改字段类型会锁表,大表操作需要谨慎(使用 pt-online-schema-change 等工具)
  • 建议新表设计时就直接使用 BIGINT,省去后续迁移的麻烦
三、解决方案二:使用分布式 ID —— 雪花算法(Snowflake)

四、Redis 面试题

共 26 题。

题目清单

  1. Redis 数据类型有哪几种?
  2. Redis 和 Memcached 的区别?
  3. Redis 和 Caffeine 的区别是什么?
  4. Redis 是单线程还是多线程?
  5. Redis 单线程,为什么还这么快?
  6. Redis Key 和 Value 的设计原则有哪些?
  7. 谈谈 Redis 的持久化机制:RDB 与 AOF 的区别?
  8. Redis 分布式锁如何实现?
  9. Redis 锁是并发安全的吗?怎么保证不被抢夺?
  10. 怎么解决 Redis 缓存和数据库的一致性问题?
  11. Redis 如何实现发布、订阅?
  12. 谈谈 Redis 5.0 中的 Stream 消息队列?
  13. Redis 如何实现延迟消息?
  14. 谈谈 Redis 的过期策略?
  15. 谈谈 Redis 的内存淘汰策略?
  16. Redis 事务和 Lua 脚本的区别?
  17. Redis 为什么要自定义 SDS?
  18. Redis 如何高效安全的遍历所有 Key?
  19. 什么是缓存击穿、缓存穿透、缓存雪崩?
  20. 什么是大 Key 问题,如何解决?
  21. 什么是热点 Key 问题,如何解决?
  22. 谈谈 ZSet 底层实现?
  23. 谈谈 Redis 集群模式?
  24. 什么是 Redis 集群脑裂问题,怎么解决?
  25. 什么是 Redis 的数据分片?
  26. 为什么 Redis 不支持回滚?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. Redis 数据类型有哪几种?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你能背出几个类型名称,更是想看你是否了解每种类型的特点和适用场景,能否在实际项目中做出合理选型。
  2. 底层原理:考察你是否理解 Redis 各数据类型的底层编码实现(如 ziplist、quicklist、hashtable 等),这体现了你对 Redis 性能优化的理解深度。
  3. 实战经验:能否结合具体业务场景说明使用哪种数据类型,体现的不是 “会用 API”,而是 “知道为什么这么用”。

核心答案

Redis 提供 5 种基础数据类型 + 4 种扩展数据类型,共计 9 种:

数据类型 底层编码 说明 典型场景
String int / embstr / raw 最基本的类型,可存字符串、整数、浮点数 缓存、计数器、分布式锁
List quicklist(双向链表 + ziplist) 有序可重复的字符串列表 消息队列、最新消息排行
Hash ziplist / hashtable 键值对集合,类似 Java 的 HashMap 存储对象、用户信息
Set intset / hashtable 无序不重复的字符串集合 标签、共同好友、去重
ZSet(Sorted Set) ziplist / skiplist + hashtable 有序不重复集合,每个元素关联一个 score 排行榜、延迟队列
扩展类型 说明 典型场景
Bitmap 位操作,基于 String 实现 签到打卡、用户活跃统计
HyperLogLog 基数估算,基于 String 实现 UV 统计(允许误差)
GEO 地理位置信息,基于 ZSet 实现 附近的人、距离计算
Stream 消息流(Redis 5.0 新增) 消息队列(比 List 更完善)

深度解析

一、五种基础数据类型
1. String(字符串)

String 是 Redis 中最简单的数据类型,一个 Key 对应一个 Value。它可以存储字符串、整数、浮点数,最大能存 512MB 的数据。

底层编码有三种:

  • int:当值为整数且不超过 long 范围时使用,8 字节长整型
  • embstr:当字符串长度 ≤ 44 字节时使用,一次内存分配,紧凑存储
  • raw:当字符串长度 > 44 字节时使用,SDS(Simple Dynamic String)实现

2. Redis 和 Memcached 的区别?

面试考察点

  1. 技术选型能力:面试官不仅仅是想让你列举几个不同点,更是想知道你在实际项目中能否根据业务需求做出合理的缓存选型决策,而不是 “领导说用啥就用啥”。
  2. 底层原理深度:考察你是否理解两者的线程模型、内存管理机制、持久化策略等核心差异,这些直接影响到生产环境的性能和稳定性。
  3. 实践经验:能否结合真实场景说明什么时候选 Redis、什么时候选 Memcached,体现的是 “用过且踩过坑” 的实战积累。

核心答案

Redis 和 Memcached 都是高性能的内存缓存中间件,但在功能丰富度、数据结构、持久化、高可用等方面存在显著差异。目前新项目绝大多数场景首选 Redis。

对比维度 Redis Memcached
数据类型 5 种基础 + 4 种扩展(String、List、Hash、Set、ZSet 等) 仅 String
持久化 支持 RDB + AOF,数据可落盘 纯内存,重启即丢
高可用 主从复制、哨兵、Cluster 集群 需要客户端一致性哈希
线程模型 核心 command 单线程(6.0 后网络 IO 多线程) 多线程
内存管理 根据数据类型选择最优编码,支持淘汰策略 Slab Allocator 机制
事务支持 事务 + Lua 脚本 CAS 原子操作
发布订阅 支持 Pub/Sub 不支持
单值大小 最大 512MB 最大 1MB

一句话结论:Memcached 是纯粹的缓存,而 Redis 是 “缓存 + 数据库” 的混合体。新项目基本都选 Redis。

深度解析

一、数据类型差异

这是最核心的区别。Memcached 只能存简单的 Key-Value(String),而 Redis 支持丰富的数据结构。

  • Memcached:只能存储 Key-Value 形式的字符串。如果要存一个用户对象,需要把整个对象 JSON 序列化后存进去,修改某个字段也得把整个对象读出来改完再写回去,既浪费带宽又浪费时间。
  • Redis:原生支持 Hash、List、Set、ZSet 等结构。比如用 Hash 存用户信息,修改年龄只需 HSET user:1001 age 26,无需读写整个对象。ZSet 更是可以直接做排行榜、延迟队列等复杂业务,Memcached 想都别想。
二、持久化能力
  • Memcached:纯内存存储,没有任何持久化机制。进程一旦重启、服务器宕机或断电,所有数据全部丢失。它就是一个纯粹的缓存,你必须在数据库里保留完整数据。
  • Redis:提供两种持久化方案。RDB(快照)定期将内存数据全量写入磁盘,恢复速度快但可能丢失最后一次快照之后的数据。AOF(追加日志)记录每一次写操作,数据安全性更高。两者可以组合使用,Redis 4.0 后还支持 RDB + AOF 混合持久化,兼顾恢复速度和数据安全。
三、线程模型对比

3. Redis 和 Caffeine 的区别是什么?

面试考察点

  1. 缓存架构理解:面试官不仅仅是想知道两者的功能差异,更是想考察你是否理解 “本地缓存” 与 “分布式缓存” 的本质区别,以及在系统架构中各自扮演的角色。
  2. 技术选型能力:考察你是否能够根据业务场景(数据量、一致性要求、性能需求)做出合理的技术选型,而不是盲目跟风。
  3. 实践经验:是否在实际项目中使用过这两种缓存,是否了解它们在生产环境中的坑和最佳实践。

核心答案

Redis 和 Caffeine 是两种不同类型的缓存,核心区别在于 存储位置 和 架构模式:

对比维度 Redis Caffeine
缓存类型 分布式缓存 本地缓存(进程内缓存)
存储位置 独立服务进程,可部署多节点 应用 JVM 进程内
访问速度 网络开销,毫秒级 直接内存访问,纳秒级
数据一致性 多节点共享,一致性好 各节点独立,存在不一致风险
容量限制 可配置大容量(GB 级别) 受 JVM 堆内存限制
持久化 支持 RDB/AOF 持久化 进程重启数据丢失
数据结构 丰富(String、Hash、List、Set、ZSet 等) 简单 KV 存储
集群支持 原生支持主从、哨兵、集群 单机,无集群概念

一句话总结:Redis 是分布式缓存,适合跨服务共享数据;Caffeine 是本地缓存,适合单节点高性能读取。

深度解析

一、架构模式对比
  1. 数据共享:多个服务实例看到的是同一份数据,天然保证一致性
  2. 独立部署:Redis 作为独立服务,可横向扩展
  3. 网络开销:每次访问都需要网络通信,存在毫秒级延迟

先看 Caffeine 的本地缓存架构:每个服务实例都有自己独立的 Caffeine 缓存,存储在各自的 JVM 堆内存中。特点是:

  1. 数据隔离:各实例缓存独立,可能存在数据不一致
  2. 极速访问:直接内存读取,纳秒级响应
  3. 容量受限:受 JVM 堆内存大小限制
二、性能对比
场景 Redis Caffeine 性能差距
单次读取 ~1ms(网络 + 反序列化) ~100ns 10000 倍
单次写入 ~1ms ~100ns 10000 倍
高并发读取 受网络带宽限制 仅受 CPU 限制 差距明显
三、典型使用场景

Redis 适合的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 分布式 Session 存储
// 2. 跨服务共享的配置数据
// 3. 分布式锁
// 4. 消息队列(延迟队列)
// 5. 排行榜、计数器
// 6. 需要持久化的缓存数据

@Autowired
private StringRedisTemplate redisTemplate;

// 分布式锁示例
public boolean tryLock(String key, String value, long expireTime) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.SECONDS)
);
}

4. Redis 是单线程还是多线程?

面试考察点

  1. 准确性认知:面试官不仅仅是想听你回答 “单线程” 或 “多线程”,更是想知道你是否能区分 Redis 的不同阶段、不同模块的线程模型,而不是一概而论。
  2. 原理深度:考察你是否理解 Redis 为什么选择单线程处理命令、单线程为什么还能这么快,以及 6.0 之后引入多线程 IO 的动机和实现方式。
  3. 版本演进意识:Redis 的线程模型在 6.0 前后发生了重大变化,能否说清楚这个演进过程,体现的是你对技术趋势的关注和知识更新的习惯。

核心答案

严格来说,Redis 一直都不是纯粹的单线程。准确的说法是:Redis 的命令执行是单线程的,但后台任务和网络 IO(6.0+)是多线程的。

版本 命令执行 网络 IO 后台任务
Redis 4.0 之前 单线程 单线程 单线程(仅有少量 bio 线程)
Redis 4.0 单线程 单线程 多线程(异步删除等后台任务)
Redis 6.0+ 单线程 多线程(可选) 多线程

一句话结论:Redis 的核心数据操作始终是单线程的,6.0 之后把网络读写变成了多线程,命令执行依然是单线程。

深度解析

一、为什么 Redis 命令执行选择单线程?

很多人第一反应是 “单线程不是更慢吗?” 恰恰相反,对于 Redis 这种基于内存的数据库,单线程反而更快,原因有三:

  • 避免上下文切换:多线程在切换时需要保存和恢复寄存器、栈帧等,每次切换消耗约 1~5 微秒。Redis 的单次命令执行也就几微秒,频繁切换反而得不偿失。单线程一路执行,没有任何切换开销。
  • 避免锁竞争:多线程操作共享数据结构(如哈希表、跳表)需要加锁,而加锁意味着阻塞和等待。Redis 的单线程天然串行执行,所有操作无需加锁,代码实现也简单得多,不容易出并发 bug。
  • 内存操作本身就极快:Redis 的数据全在内存里,单次命令执行一般在微秒级。CPU 根本不是瓶颈,真正的瓶颈在网络 IO。所以用单线程处理命令完全够了,没必要为了 “并行” 引入不必要的复杂度。
二、Redis 的多线程演进历程

5. Redis 单线程,为什么还这么快?

面试考察点

  1. 原理理解深度:面试官不仅仅是想听你列举几个原因,更是想知道你是否真正理解 Redis 的性能来源 —— 是内存速度、IO 多路复用、还是高效数据结构?哪个才是核心因素?
  2. 系统级知识广度:这道题会涉及到操作系统(epoll、文件描述符、内核态/用户态)、网络编程(Reactor 模式)、数据结构(SDS、跳表、压缩列表)等多方面知识,体现你的技术功底。
  3. 对比分析能力:能否从 “为什么不用多线程” 的反面角度来论证单线程设计的合理性,体现你的架构思维。

核心答案

Redis 单线程还能达到 10 万+ QPS,核心原因可以归纳为 4 个方面:

快的原因 一句话解释 重要程度
基于内存操作 数据在内存中,纳秒级访问,不碰磁盘 ⭐⭐⭐⭐⭐
IO 多路复用 一个线程管理海量连接,不阻塞 ⭐⭐⭐⭐⭐
高效的数据结构 SDS、ziplist、skiplist 等专为性能设计 ⭐⭐⭐⭐
单线程避免开销 无锁竞争、无上下文切换、无并发 bug ⭐⭐⭐⭐

一句话结论:Redis 快的本质是 内存 + IO 多路复用,单线程反而是加分项(避免了锁和切换开销),而不是拖后腿。

深度解析

一、基于内存操作(最核心的原因)

这是 Redis 快的 最根本原因,没有之一。

  • 内存 vs 磁盘的量级差距:内存访问约 100 纳秒,SSD 约 0.1 毫秒,差了 1000 倍;机械硬盘约 10 毫秒,差了 10 万倍。Redis 的数据全部存在内存中,每次读写就是一次内存操作,这是它快的物理基础。
  • 传统数据库的瓶颈:MySQL 即使有 Buffer Pool 把热数据缓存到内存,但写入时要写 WAL 日志(fsync 刷盘)、定期刷脏页回磁盘,这些磁盘 IO 是绕不开的。Redis 则完全不需要碰磁盘(除非开启持久化),纯内存操作。
  • Redis 的持久化是异步的:即使开启了 RDB 或 AOF,持久化操作也是由后台线程/子进程完成的,不会阻塞主线程的命令执行。
二、IO 多路复用(高性能的网络模型)

光有内存快还不够,如果每个客户端连接都阻塞等待,性能也上不去。Redis 使用 IO 多路复用 技术,让一个线程就能高效管理数万个客户端连接。


五、RocketMQ 面试题

共 8 题。

题目清单

  1. 为什么项目要选择 RocketMQ?
  2. RocketMQ 的架构是怎么样的?
  3. RocketMQ 有几种集群部署方式?
  4. RocketMQ 消息堆积了怎么处理?
  5. RocketMQ 的消息是推模式,还是拉模式?
  6. 谈谈 RocketMQ 的事务消息?
  7. RocketMQ 怎么保证消息的顺序性?
  8. RocketMQ 怎么保证消息不丢失?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 为什么项目要选择 RocketMQ?

面试考察点

  1. 技术选型能力:面试官不仅仅是想知道 RocketMQ 有什么优点,更是想知道你是否具备多维度对比分析的能力,能否根据业务场景特点给出合理的技术选型依据。
  2. 消息中间件全局认知:考察你是否了解主流消息队列(RocketMQ、Kafka、RabbitMQ)的定位差异,是否只是 “用过” 还是真正理解各自的设计哲学和适用边界。
  3. 实践与原理结合:能否结合项目实际需求(如事务消息、顺序消息、高可用等),讲清楚 “为什么 RocketMQ 是最佳选择” 而非泛泛而谈。

核心答案

选择 RocketMQ 的核心原因可以概括为 **”三高一丰”**:

维度 核心优势 对比其他 MQ
高吞吐 单机 TPS 可达 10 万+ 优于 RabbitMQ,与 Kafka 接近
高可靠 同步双写 + 同步刷盘,消息零丢失 优于 Kafka(异步刷盘可能丢消息)
高一致 原生事务消息支持 Kafka 和 RabbitMQ 均不原生支持
功能丰富 延迟消息、顺序消息、消息回溯、过滤 功能维度全面领先

一句话总结:如果你的项目是 Java 技术栈、对消息可靠性要求高、需要事务消息或延迟消息等高级特性,RocketMQ 是最优选择。

深度解析

一、主流消息队列横向对比

上表展示了三大主流消息队列的核心差异。从对比中可以看出:

  • RocketMQ vs Kafka:Kafka 定位是大数据领域的消息流平台,追求极致吞吐,但在业务消息场景(如订单、支付)下的可靠性、功能丰富度不如 RocketMQ。RocketMQ 天生为业务消息而生。
  • RocketMQ vs RabbitMQ:RabbitMQ 延迟最低(us 级),但单机吞吐量上限低(万级),且 Erlang 语言栈对 Java 团队不友好,排查问题困难。RocketMQ 吞吐量高出一个数量级,且全 Java 技术栈便于深入定制。
二、选择 RocketMQ 的六大核心理由
理由 1:原生事务消息——分布式事务的优雅方案

这是 RocketMQ 最核心的杀手级特性。以电商下单场景为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ============ 生产者:发送事务消息 ============
TransactionMQProducer producer = new TransactionMQProducer("tx_producer_group");
// 设置事务监听器,用于执行本地事务和回查
producer.setTransactionListener(new TransactionListener() {

// 执行本地事务(比如:扣减库存、创建订单)
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 执行本地业务逻辑
orderService.createOrder(parseOrder(msg));
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}

// 回查本地事务状态(broker 长时间未收到确认时触发)
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
Order order = orderService.getByOrderId(parseOrderId(msg));
// 订单存在说明本地事务已提交 → 提交消息
return order != null ? LocalTransactionState.COMMIT_MESSAGE
: LocalTransactionState.ROLLBACK_MESSAGE;
}
});

Message msg = new Message("ORDER_TOPIC", orderJson.getBytes());
// 发送事务消息(半消息)
producer.sendMessageInTransaction(msg, null);

2. RocketMQ 的架构是怎么样的?

面试考察点

  1. 全局架构认知:面试官不仅仅是想知道 RocketMQ 有几个组件,更是想知道你是否理解各组件的职责边界和协作关系,能否从宏观角度把握整个消息系统的设计。
  2. 设计思想理解:考察你是否理解 RocketMQ 为什么这么设计——比如为什么用 NameServer 而不是 ZooKeeper、为什么 Broker 要分 Master/Slave、Topic 和 Queue 的关系是什么。懂 “为什么” 比 “是什么” 更重要。
  3. 高可用感知:架构设计的核心目标之一是高可用,面试官想看你是否知道 RocketMQ 是如何通过架构保证高可用的(主从切换、消息重试、故障隔离等)。

核心答案

RocketMQ 的整体架构由 四大核心组件 构成,采用去中心化的设计理念:

组件 角色 类比 核心职责
NameServer 路由注册与发现 注册中心 管理 Broker 路由信息,无状态,互相独立
Broker 消息存储与转发 消息服务器 接收、存储、投递消息,分 Master/Slave
Producer 消息发送者 寄件人 发送消息,支持多种发送方式
Consumer 消息消费者 收件人 拉取/推送消息,支持集群/广播消费

一句话总结:Producer 和 Consumer 通过 NameServer 发现 Broker 地址,Broker 负责消息的存储和转发,NameServer 各节点独立、无状态,整体架构去中心化、高可用。

深度解析

一、整体架构全景图
  • 接入层:Producer 负责发送消息,Consumer 负责消费消息,它们都是客户端。
  • 路由层:NameServer 集群充当路由注册中心,每个节点独立运行,互不通信,无状态设计。
  • 存储层:Broker 集群负责消息的存储和转发,每个 Broker 分 Master(主)和 Slave(从),通过同步/异步复制保证数据可靠性。

消息流转路径:Producer → NameServer(获取路由)→ Master Broker(存储消息)→ Consumer(拉取消费)。

二、四大组件深度剖析
1. NameServer——轻量级注册中心
1
2
3
4
5
6
7
8
9
10
11
// NameServer 核心数据结构(源码简化)
public class RouteInfoManager {
// Topic → QueueData 的映射(一个 Topic 分布在哪些 Broker 上)
HashMap<String/* topic */, Map<String /* brokerName */, QueueData>> topicQueueTable;

// Broker 地址信息
HashMap<String/* brokerName */, BrokerData> brokerAddrTable;

// Broker 存活信息(心跳检测)
HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
}

3. RocketMQ 有几种集群部署方式?

面试考察点

  1. 架构设计能力:面试官不仅仅是想知道有几种部署方式,更是想考察你是否理解不同部署模式在可靠性、吞吐量、延迟之间的权衡,能否根据业务 SLA 要求做出合理选型。
  2. 高可用理解深度:同步复制和异步复制的区别是什么?Master 宕机后 Slave 如何接管?Dledger 自动选主原理是什么?这些问题直指你对 RocketMQ 高可用机制的底层理解。
  3. 生产实践意识:能否说出你们生产环境的部署模式?为什么这么选?踩过什么坑?这考察的是真实项目经验,而非纸上谈兵。

核心答案

RocketMQ 主要有 5 种集群部署方式:

部署模式 可靠性 吞吐量 Master 宕机影响 推荐场景
单 Master ❌ 极低 高 整体不可用 仅本地测试
多 Master ⚠️ 中等 最高 仅影响该 Broker 上的 Topic 允许丢消息的日志场景
多 Master 多 Slave(异步复制) ✅ 较高 高 Slave 自动接管消费,短暂不可写入 通用业务场景
多 Master 多 Slave(同步双写) ✅✅ 极高 较低(同步开销) Slave 无缝接管,消息零丢失 金融、交易等核心链路
Dledger / Controller 模式 ✅✅ 极高 较高 自动选主,秒级切换 追求自动故障恢复的生产环境

一句话总结:生产环境最常用的是 多 Master 多 Slave 同步双写模式,配合 Dledger 或 RocketMQ 5.0 的 Controller 模式实现自动故障切换。

深度解析

一、模式一:单 Master
  • 架构:只有一个 Broker 节点,所有消息都写入这个节点。
  • 优点:部署最简单,配置最少,适合快速验证功能。
  • 致命缺点:单点故障。Broker 一旦宕机,整个消息系统不可用,所有未消费的消息都无法恢复。
  • 适用场景:仅限本地开发测试,绝对不能用于生产环境。
二、模式二:多 Master
  • 架构:多个 Broker 节点都是 Master,没有 Slave。Topic 的 Queue 分布在不同 Master 上,Producer 轮询发送实现负载均衡。
  • 优点:
    • 吞吐量最高——没有同步复制的开销,所有 Master 独立写入
    • 单个 Master 宕机不影响其他 Master 上的消息收发
    • 部署运维相对简单
  • 缺点:
    • 宕机的 Master 上的 Queue 不可用,该部分消息无法投递和消费
    • 没有数据副本,Master 磁盘故障会导致消息永久丢失
    • 宕机恢复后需要手动重启,没有自动故障切换
  • 适用场景:日志采集、监控数据上报等允许少量丢消息、对可用性要求不极端的场景。
三、模式三:多 Master 多 Slave——异步复制

六、MyBatis 面试题

共 13 题。

题目清单

  1. MyBatis 和 Hibernate 有什么区别?
  2. #{} 和 ${} 的区别?
  3. 说说 Mybatis 的缓存机制?
  4. Mybatis 是怎么实现字段映射的?
  5. 说说 Mybatis 插件的运行原理?
  6. MyBatis 支持动态 SQL 吗?
  7. 谈谈 Mybatis 的工作原理?
  8. Mybatis 是否支持延迟加载?实现原理是什么?
  9. MyBatis 如何执行批量操作?
  10. MyBatis 是如何进行分页的?分页插件的原理是什么?
  11. 为什么 Mapper 接口不需要写实现类?
  12. Mybatis 都有哪些 Executor 执行器?
  13. 说说 JDBC 的执行步骤?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. MyBatis 和 Hibernate 有什么区别?

面试考察点

  1. 框架认知广度:面试官不仅仅是想知道你 “用过哪个”,更是想考察你对主流 ORM 框架的理解是否全面,能否站在架构层面做出技术选型。
  2. 底层原理理解:考察你是否清楚两者的核心机制差异——Hibernate 的全自动 ORM vs MyBatis 的半自动映射,以及这对 SQL 控制力、性能调优的影响。
  3. 生产实践意识:看你能否结合项目经验,说出在不同场景下选择哪个框架更合适,而不是简单地说 “哪个都行”。

核心答案

MyBatis 和 Hibernate 本质上是两种不同哲学的 ORM 框架,核心区别如下:

对比维度 MyBatis Hibernate
定位 半自动 ORM(SQL 映射框架) 全自动 ORM 框架
SQL 控制力 手写 SQL,控制力极强 自动生成 SQL,控制力较弱
学习成本 低,上手快 高,概念多(Session、Cache、HQL 等)
数据库迁移 差,SQL 与数据库耦合 好,通过 Dialect 屏蔽差异
缓存机制 简单的一级缓存 + 可选二级缓存 完善的一级缓存 + 二级缓存 + 查询缓存
适合场景 复杂 SQL、性能调优、报表统计 快速开发、CRUD 为主、多数据库项目
国内使用率 极高,互联网公司主流 较低,传统企业 / 外企较多

一句话总结:MyBatis 赢在灵活和可控,Hibernate 赢在高效和通用。国内互联网公司绝大多数选择 MyBatis。

深度解析

一、架构设计理念对比
  • MyBatis:开发者自己写 SQL,框架只负责参数映射和结果集映射。开发者对 SQL 有 100% 的控制权,可以针对特定数据库做极致优化。
  • Hibernate:开发者操作的是 Java 对象(POJO),框架根据映射配置自动生成 SQL。好处是开发者可以完全不用关心 SQL,坏处是自动生成的 SQL 可能不够高效,复杂查询优化困难。
二、核心机制差异
1. SQL 控制:手写 vs 自动

MyBatis 示例:

1
2
3
4
5
6
7
8
9
<!-- 开发者完全掌控 SQL,可以写出最优查询 -->
<select id="findActiveUsers" resultType="User">
SELECT id, username, email, created_time
FROM t_user
WHERE status = 1
AND created_time > #{startTime}
ORDER BY created_time DESC
LIMIT #{pageSize}
</select>

Hibernate 示例:

2. #{} 和 ${} 的区别?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你 “用过” 这两个符号,更是想考察你是否理解它们底层的处理机制——预编译 vs 字符串拼接。
  2. 安全意识:这是这道题的核心。考察你是否具备 SQL 注入防范意识,以及能否在开发中做出正确选择。
  3. 实战经验:看你能否说出 ${} 的合理使用场景(动态表名、动态排序等),而不是一刀切地说 “永远别用 ${}“。

核心答案

#{} 和 ${} 的核心区别在于 SQL 参数的替换方式不同:

对比维度 #{} ${}
替换方式 预编译处理,使用 ? 占位符 字符串拼接,直接替换到 SQL 中
SQL 注入 安全,自动转义 危险,存在注入风险
性能 预编译 SQL 可缓存复用 每次生成不同的 SQL,无法复用
适用场景 传参数值(WHERE 条件等) 动态表名、列名、ORDER BY 等
类型处理 自动处理类型(加引号等) 原样拼接,不处理类型

一句话结论:能用 #{} 就用 #{},只有动态表名、动态列名、ORDER BY 等场景才用 ${}。

深度解析

一、底层机制对比
  • #{} 的处理流程:MyBatis 在解析 SQL 时,会将 #{id} 替换为 ? 占位符,然后通过 PreparedStatement.setInt() 等方法设置参数值。数据库先编译 SQL 结构,再绑定参数,参数值不会参与 SQL 编译,因此不可能改变 SQL 的语法结构。
  • ${} 的处理流程:MyBatis 在解析 SQL 时,直接将 ${id} 替换为参数的字符串值,拼接到 SQL 中。参数值参与了 SQL 编译,恶意输入可以改变 SQL 的语法结构,导致 SQL 注入。
二、SQL 注入实战演示

来看一个典型的 SQL 注入场景:

1
2
3
4
<!-- 危险!使用 ${} 拼接用户名 -->
<select id="findByUsername" resultType="User">
SELECT * FROM t_user WHERE username = '${username}'
</select>

假设恶意用户输入:username = "admin' OR '1'='1"

最终执行的 SQL 变成:

1
2
-- 原本只想查一个用户,结果变成了查全部用户!
SELECT * FROM t_user WHERE username = 'admin' OR '1'='1'

同样的场景,使用 #{}:

1
2
3
4
<!-- 安全!使用 #{} 预编译 -->
<select id="findByUsername" resultType="User">
SELECT * FROM t_user WHERE username = #{username}
</select>

不管用户输入什么,最终 SQL 结构始终是:

3. 说说 Mybatis 的缓存机制?

面试考察点

  1. 框架原理理解:面试官不仅仅是想知道你 “听说过” 一级缓存和二级缓存,更是想考察你是否理解 MyBatis 缓存的层级结构、作用范围、失效策略等底层机制。
  2. 生产踩坑意识:MyBatis 缓存在实际开发中容易踩坑(比如二级缓存脏读问题),面试官想知道你是否遇到过这些问题,以及如何解决。
  3. 架构设计思维:看你能否从缓存层次、作用域、生命周期等维度系统性地分析问题,而不是零散地背概念。

核心答案

MyBatis 提供了 两级缓存机制,用于减少数据库访问次数,提升查询性能:

维度 一级缓存 二级缓存
作用范围 SqlSession 级别 namespace(Mapper)级别
默认状态 默认开启,无法关闭 默认关闭,需手动开启
存储位置 HashMap(内存) 可自定义(内存 / Redis 等)
生命周期 随着 SqlSession 关闭而销毁 随着 ApplicationContext 存活
跨 Session 不可以 可以
失效条件 增删改操作、手动清空、关闭 增删改操作(仅影响同一 namespace)

一句话结论:一级缓存是 SqlSession 级别的本地缓存,默认开启;二级缓存是 namespace 级别的全局缓存,需要手动开启。实际生产中,很多团队选择关闭二级缓存,改用 Redis 等外部缓存。

深度解析

一、缓存整体架构

MyBatis 的缓存查询遵循 “先查二级,再查一级,最后查数据库” 的顺序:

  • 第一步:查询二级缓存。二级缓存是 namespace 级别的,所有 SqlSession 共享。如果命中,直接返回结果。
  • 第二步:查询一级缓存。二级缓存未命中时,进入当前 SqlSession 的一级缓存。如果命中,直接返回结果,并且将结果写入二级缓存。
  • 第三步:查询数据库。两级缓存都没命中,执行 SQL 查询数据库,结果写入一级缓存。当 SqlSession 关闭(close())或提交(commit())时,一级缓存的数据才会写入二级缓存。
二、一级缓存详解

一级缓存基于 SqlSession,由 PerpetualCache 实现,底层就是一个简单的 HashMap。

核心特性:

  • 同一个 SqlSession 中,执行相同的 SQL 和参数,第二次直接从缓存取
  • 默认开启,无法关闭
  • 执行增删改操作后,缓存自动清空
  • 调用 sqlSession.clearCache() 手动清空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 一级缓存示例
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

// 第一次查询:查数据库,结果写入一级缓存
User user1 = mapper.selectById(1L);

// 第二次查询:相同的 SQL 和参数,直接命中一级缓存
User user2 = mapper.selectById(1L);

System.out.println(user1 == user2); // true,同一个对象引用!

// 执行增删改操作,一级缓存自动清空
mapper.updateById(new User(1L, "newName"));

// 第三次查询:缓存已清空,重新查数据库
User user3 = mapper.selectById(1L);
System.out.println(user1 == user3); // false,不同的对象

4. Mybatis 是怎么实现字段映射的?

面试考察点

  1. 映射机制理解:面试官不仅仅是想知道你 “会用” resultMap,更是想考察你是否理解 MyBatis 是如何将数据库字段(下划线命名)映射到 Java 对象属性(驼峰命名)的,底层原理是什么。
  2. 配置灵活度:看你能否说出多种映射方式(自动映射、resultMap、注解),以及各自的适用场景和优劣。
  3. 复杂映射能力:考察你是否掌握一对一、一对多等复杂关联映射的配置方式,这是实际开发中非常常见的需求。

核心答案

MyBatis 的字段映射本质上是将 JDBC ResultSet 中的列 转换为 Java 对象的属性,主要提供了 4 种映射方式:

映射方式 适用场景 灵活度 推荐度
自动映射(驼峰转换) 字段名和属性名符合驼峰规则 低 简单查询推荐
resultType + 别名 字段名和属性名不一致但不多 中 快速开发
resultMap 手动映射 复杂映射、一对一、一对多 高 生产推荐
注解 @Results 不想写 XML 中 偏好注解的团队

一句话结论:MyBatis 通过 ResultSet 元数据获取列名,再通过反射将值设置到 Java 对象的对应属性上。简单查询用自动映射,复杂映射用 resultMap。

深度解析

一、字段映射的整体流程

MyBatis 字段映射的核心流程可以拆解为以下几个关键步骤:

  • 步骤一(SQL 执行):MyBatis 解析 Mapper XML 中的 SQL,通过 JDBC 执行查询,获得 ResultSet 结果集。
  • 步骤二(遍历结果集):逐行遍历 ResultSet,每一行都需要映射为一个 Java 对象。
  • 步骤三(映射策略选择):根据配置决定使用 resultType 自动映射还是 resultMap 手动映射。
  • 步骤四(核心映射操作):这是最关键的一步,包含 4 个子操作——获取列名、匹配属性、类型转换、反射赋值。其中 TypeHandler 负责处理数据库类型和 Java 类型之间的转换(比如 VARCHAR → String,BIGINT → Long)。
二、4 种映射方式详解
方式一:自动映射(驼峰转换)

数据库字段通常用下划线命名(user_name),Java 属性用驼峰命名(userName)。MyBatis 提供了自动驼峰转换功能:

1
2
3
4
<!-- mybatis-config.xml 开启驼峰转换(默认开启) -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

七、Spring Cloud 面试题

共 11 题。

题目清单

  1. 什么是微服务?它带来哪些挑战?
  2. 什么是 Spring Cloud,它有哪些组件?
  3. 为什么需要 Gateway 网关,它有什么作用?
  4. Zuul、Gateway 和 Nginx 有什么区别?
  5. Dubbo 和 Feign 有什么区别?
  6. 说说 HTTP 和 RPC 的区别?
  7. LoadBalancer 和 Ribbon 的区别是什么?为什么用它替代 Ribbon?
  8. Feign 第一次调用为什么会很慢?怎么解决?
  9. Nacos 与 Eureka、ZooKeeper 的区别?
  10. Nacos 如何实现服务注册与发现的?
  11. Hystrix 和 Sentinel 的区别?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 什么是微服务?它带来哪些挑战?

面试考察点

  1. 架构认知:面试官不仅仅是想知道你能不能背出微服务的定义,更是想知道你是否理解微服务架构出现的背景,以及它解决了单体架构的什么痛点。
  2. 全局视野:考察你是否清楚微服务在带来好处的同时,也引入了大量复杂的分布式问题——只会说好处、不说挑战,说明理解不够深入。
  3. 实践意识:考察你在真实项目中是否踩过微服务的坑,对服务治理、链路追踪、配置管理等是否有实战经验。

核心答案

微服务是一种架构风格,将一个大型单体应用拆分为多个小型、独立部署的服务,每个服务围绕特定业务能力构建,拥有自己独立的数据库,服务之间通过轻量级通信机制(通常是 HTTP REST 或消息队列)进行协作。

一句话概括:把一个大系统拆成一堆小系统,各自独立开发、独立部署、独立运行,通过 API 互相通信。

微服务带来的主要挑战:

挑战领域 核心问题 复杂度
服务治理 服务如何注册、发现、路由、负载均衡 ⭐⭐⭐
分布式通信 网络延迟、超时、重试、熔断降级 ⭐⭐⭐⭐
数据一致性 跨服务事务、最终一致性保障 ⭐⭐⭐⭐⭐
运维复杂度 部署、监控、日志、链路追踪 ⭐⭐⭐⭐
配置管理 多服务配置统一管理和动态刷新 ⭐⭐⭐
团队协作 服务边界划分、API 版本管理 ⭐⭐⭐

深度解析

一、单体架构 vs 微服务架构
  • **从 “一个应用” 变成 “多个服务”**:每个服务独立进程、独立部署
  • **从 “共享数据库” 变成 “每个服务独享数据库”**:避免数据层耦合
  • **从 “方法调用” 变成 “网络通信”**:服务间通过 RPC 交互
二、微服务带来的核心挑战
挑战 1:分布式通信问题

单体架构中,模块之间是方法调用,可靠且快速。微服务中,服务间是网络通信,会面临:

  • 网络延迟:原来本地方法调用是微秒级,现在变成毫秒甚至秒级
  • 网络不可靠:调用可能超时、失败,需要重试机制
  • 服务不可用:下游服务挂了,上游不能跟着一起挂

解决方案:引入熔断降级(如 Sentinel、Hystrix)、超时重试、限流等机制。

挑战 2:数据一致性

这是微服务最难的问题。举个例子:

用户下单 → 需要调用库存服务扣库存 → 调用支付服务扣款 → 调用订单服务创建订单

这三个操作分别在三个服务中,各自有独立的数据库,没法用传统的数据库事务来保证一致性。

解决方案:

  • 最终一致性:通过消息队列 + 本地消息表实现
  • Saga 模式:将长事务拆成多个本地事务,通过补偿机制回滚
  • Seata:阿里开源的分布式事务框架
挑战 3:运维复杂度爆炸

单体应用部署一个 jar 包就行了,微服务呢?

  • 一个系统可能有 几十甚至上百个服务
  • 每个服务需要独立部署、独立扩缩容
  • 出了问题需要链路追踪才能定位是哪个服务导致的

2. 什么是 Spring Cloud,它有哪些组件?

面试考察点

  1. 框架全局认知:面试官不仅仅是想知道你背了几个组件名字,更是想知道你是否理解 Spring Cloud 在微服务架构中的定位——它是一个 “工具箱”,而不是某一个具体技术。
  2. 技术选型能力:考察你是否了解 Spring Cloud 的版本演进,知道 Netflix 体系已逐步停更,Alibaba 体系是当前国内主流,能否说出它们之间的差异。
  3. 实战经验:如果你只是在简历上写了 “熟悉 Spring Cloud”,却说不清各组件在生产环境怎么搭配使用,说明只是 “听说过” 而非 “用过”。

核心答案

Spring Cloud 是 Spring 官方提供的一套微服务架构工具集,它基于 Spring Boot 开发,为开发者提供了一整套开箱即用的微服务解决方案,包括服务注册与发现、配置管理、负载均衡、熔断降级、API 网关、分布式消息等。

一句话概括:**Spring Cloud 不是某一个框架,而是一堆微服务组件的 “全家桶”**。

Spring Cloud 核心组件一览
功能领域 Netflix 体系(第一代) Alibaba 体系(第二代) 说明
服务注册与发现 Eureka Nacos 管理服务的注册和发现
配置中心 Spring Cloud Config Nacos Config 集中管理各服务配置
负载均衡 Ribbon Spring Cloud LoadBalancer 客户端负载均衡
服务间调用 Feign OpenFeign 声明式 HTTP 客户端
熔断降级 Hystrix Sentinel 服务容错与流量控制
API 网关 Zuul Spring Cloud Gateway 统一入口、路由转发
分布式事务 — Seata 跨服务事务一致性

重要趋势:Netflix OSS 体系(Eureka、Hystrix、Zuul 等)已逐步停更维护,国内企业目前主流采用 Spring Cloud Alibaba 体系。

深度解析

一、Spring Cloud 整体架构
  • 接入层:API 网关作为统一入口,负责路由转发、身份认证、限流等
  • 服务层:各个微服务独立部署,拥有各自的数据库,服务间通过 OpenFeign 或消息队列通信
  • 治理层:注册中心、配置中心、熔断降级等基础设施组件,保障服务的高可用

关键点在于,各微服务启动时会将自己的地址注册到注册中心,调用方通过注册中心获取目标服务地址,再由负载均衡器选择一个实例发起调用。

二、各核心组件详解
1. Nacos —— 注册中心 + 配置中心(二合一)

Nacos 是 Spring Cloud Alibaba 体系中最核心的组件,同时承担了服务注册发现和配置管理两个职责。

服务注册发现的工作流程:

3. 为什么需要 Gateway 网关,它有什么作用?

面试考察点

  1. 架构设计思维:面试官不仅仅是想知道你能不能搭一个 Gateway,更是想知道你是否理解网关在微服务架构中的定位——它是整个系统的 “统一大门”,是所有外部请求的必经之路。
  2. 功能掌握深度:考察你是否清楚网关不仅仅是个 “路由转发器”,还承担着鉴权、限流、熔断、日志等职责,能否结合实际场景说明。
  3. 技术选型能力:能否说出 Spring Cloud Gateway 与 Zuul 的本质区别,以及为什么 Gateway 能成为主流选择。

核心答案

没有网关的微服务架构,就像一栋大楼没有大门和前台——每个来访者都可以直接敲任何一扇门,混乱且危险。

网关的核心作用就是为微服务系统提供一个 统一的入口,将所有非业务逻辑的公共功能集中到一处处理,让各个微服务专注于业务本身。

网关的五大核心作用
作用 说明 对应技术
路由转发 根据请求路径将请求转发到对应的微服务 Predicate + Route
身份认证 统一校验 Token、权限,不用每个服务都鉴权一遍 GlobalFilter
限流熔断 保护后端服务不被突发流量压垮 Sentinel / RequestRateLimiter
日志监控 统一记录请求日志、响应时间、状态码 GlobalFilter
协议转换 外部 HTTP → 内部 RPC,或反向 自定义 Filter

一句话总结:网关就是微服务的 “门面 + 保安 + 流量调度员”。

深度解析

一、为什么没有网关不行?
  • 没有网关:客户端直连各个微服务,每个服务都要自己处理鉴权、跨域、限流等公共逻辑,导致大量重复代码,且服务地址暴露在外
  • 有网关:所有请求先经过网关,由网关统一处理公共逻辑,再路由到对应服务。各个微服务只需关注业务,服务地址也对外隐藏

关键点在于,网关本质上是将横切关注点(鉴权、限流、日志等)从各个微服务中剥离出来,集中到一处管理。

二、Spring Cloud Gateway 核心概念

Gateway 的编程模型围绕三个核心概念构建:

  • Route(路由):一个路由规则包含一个 ID、一个目标 URI、一组断言和一组过滤器。可以理解为 “一条转发规则”
  • Predicate(断言):匹配条件,比如 Path=/order/** 表示只有 /order/ 开头的请求才会匹配这条路由。支持 Path、Method、Header、Query 等多种匹配方式
  • Filter(过滤器):分为 Pre(前置)和 Post(后置)两种,在请求转发前和响应返回后执行自定义逻辑,比如鉴权、加请求头、记录日志等

完整链路:请求进来 → 遍历所有 Route → 找到 Predicate 匹配的路由 → 执行 Pre Filter → 转发请求到目标服务 → 执行 Post Filter → 返回响应。

4. Zuul、Gateway 和 Nginx 有什么区别?

面试考察点

  1. 技术选型能力:面试官不仅仅是想知道你能背出三者的区别,更是想知道你在实际项目中能否根据场景选择合适的网关方案,而不是 “领导说用啥就用啥”。
  2. 架构分层理解:考察你是否理解这三者并不是互斥的竞争关系,而是可以分层次配合使用的——Nginx 在外层做流量入口,Gateway 在内层做微服务网关。
  3. 底层原理认知:能否从 I/O 模型、编程模型、性能瓶颈等维度分析差异,而不是只停留在 “功能对比” 层面。

核心答案

这三者的定位完全不同,不存在 “谁替代谁” 的关系,而是分属不同层次:

维度 Nginx Zuul Spring Cloud Gateway
定位 通用反向代理 / Web 服务器 微服务网关(第一代) 微服务网关(第二代)
开发语言 C Java Java
I/O 模型 epoll(异步非阻塞) Zuul 1.x:同步阻塞;Zuul 2.x:异步非阻塞 Netty + Reactor(异步非阻塞)
性能 极高(C 语言 + epoll) 一般 高
微服务集成 ❌ 无 ✅ 内置服务发现 ✅ 内置服务发现
动态路由 需修改配置并 reload ✅ 支持 ✅ 支持
过滤器扩展 Lua(OpenResty) Java Java
维护状态 活跃 ❌ 已停更 ✅ Spring 官方主推
适用层次 最外层流量入口 微服务网关层 微服务网关层

一句话总结:Nginx 是 “大门保安”,负责流量分发和静态资源;Gateway 是 “楼层前台”,负责微服务路由和业务鉴权;Zuul 是 “退休的前台”,已经没人用了。

深度解析

一、三者在架构中的定位
  • Nginx 在最外层:作为整个系统的流量入口,处理 SSL、静态资源、跨域等通用逻辑,通过 upstream 将请求分发到多个 Gateway 实例
  • Gateway 在中间层:作为微服务网关,从注册中心获取服务列表,动态路由到各个微服务,同时处理鉴权、限流等业务相关逻辑
  • Zuul 已经出局:不再出现在新架构中

关键点在于,Nginx 和 Gateway 不是 “二选一” 的关系,而是 Nginx 在外、Gateway 在内的分层协作。各司其职,不要让 Gateway 去扛 Nginx 该干的活。

二、Zuul 1.x vs Zuul 2.x vs Gateway 底层对比

这三个的 I/O 模型差异是面试重点:


八、Java 并发面试题

共 31 题。

题目清单

  1. Java 线程池有几种创建方式?
  2. 线程池的拒绝策略有哪些?
  3. 线程数设置多少合适?
  4. 说说动态线程池实现原理?
  5. 任务特别多,线程池队列满了怎么办?不能拒绝!
  6. 多线程上下文切换是什么意思?
  7. 什么是守护线程,和普通线程有什么区别?
  8. 线程有几种状态,状态之间是怎样流转的?
  9. 创建线程有几种方式?
  10. 并发和并行的区别?
  11. Java 中常见的锁有哪些?
  12. 什么是线程死锁,如何排查?如何解决?
  13. 线程、进程、协程的区别?
  14. 什么是 ThreadLocal,如何实现的?
  15. ThreadLocal 为什么会导致内存泄漏?怎么解决?
  16. ThreadLocal 使用场景有哪些?
  17. ThreadLocalMap 和 HashMap 的区别?
  18. volatile 可以保证原子性吗?
  19. volatile 是如何保证可见性和有序性的?
  20. synchronized 是怎么实现的?
  21. synchronized 锁升级过程是怎样的?
  22. synchronized 和 ReentrantLock 的区别?
  23. LongAdder 和 AtomicLong 的区别?
  24. 公平锁和非公平锁的区别?
  25. 父子线程之间如何共享、传递数据?
  26. 什么是 CAS?存在什么问题?
  27. AQS 是什么?它存在什么问题?
  28. CountDownLatch、CyclicBarrier、Semaphore 的区别?
  29. 什么是 Java 内存模型(JMM)?
  30. 什么是 happens-before 原则?
  31. 什么是可重入锁,如何实现可重入锁?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. Java 线程池有几种创建方式?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你会不会创建线程池,更是想知道你是否清楚 Java 并发包中线程池的分类和适用场景,能否根据业务特点选择合适的类型。
  2. 生产实践意识:考察你是否了解 Executors 工厂方法的潜在风险(无界队列导致 OOM、无限线程导致系统崩溃),以及为什么阿里开发手册明令禁止在生产环境使用。
  3. 原理理解深度:如果你只会调用 API 创建,但不理解线程池的执行流程(核心线程 → 队列 → 非核心线程 → 拒绝)和 7 个核心参数的含义,说明只是 “会用” 而非 “懂原理” 。

核心答案

Java 中创建线程池主要有 4 种方式:

创建方式 核心类 适用场景 推荐指数
Executors 工厂方法 Executors 快速原型、测试 ⚠️ 禁止生产使用
手动构造 ThreadPoolExecutor 通用业务场景 ✅ 强烈推荐
定时任务 ScheduledThreadPoolExecutor 延迟/周期执行 ✅ 推荐
分治计算 ForkJoinPool 递归分解、并行计算 ✅ 特定场景

一句话总结:生产环境必须使用 ThreadPoolExecutor 手动构造,避免 Executors 的隐患。

深度解析

一、线程池执行流程

在介绍创建方式之前,必须先理解线程池的执行流程,否则参数配置就是 “盲人摸象” 。

图示讲解:

  1. 步骤 1 - 判断核心线程:当有新任务提交时,首先判断当前线程数是否小于核心线程数(corePoolSize)。如果是,则直接创建新的核心线程来执行任务,不需要排队。这个阶段是 “有人就干活” 。
  2. 步骤 2 - 加入队列:如果当前线程数已经达到核心线程数,新任务会尝试加入工作队列(workQueue)。队列起到了 “缓冲” 的作用,让任务先排队等待。
  3. 步骤 3 - 创建非核心线程:如果队列也满了,且当前线程数小于最大线程数(maximumPoolSize),则会创建非核心线程来执行任务。这是 “人手不够就招临时工” 的阶段。
  4. 步骤 4 - 执行拒绝策略:如果队列满了,线程数也达到最大值,就会执行拒绝策略。这是 “实在处理不了就拒绝” 的阶段。

关键点:线程池 不是 “先把线程创建满,再排队” ,而是按照 核心线程 → 队列 → 非核心线程 → 拒绝 的顺序处理任务。这个顺序很重要,它决定了队列的容量会直接影响何时创建非核心线程。

二、方式 1:Executors 工厂方法(⚠️ 生产环境禁止使用)

Executors 类提供了 4 种快捷创建方式,看起来很方便,但都存在严重隐患:

1
2
3
4
5
6
7
8
9
10
11
// ❌ 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(10);

// ❌ 缓存线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();

// ❌ 单线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();

// 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(5);

2. 线程池的拒绝策略有哪些?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道有哪几种拒绝策略,更是想看你是否能说清楚每种策略的 具体行为差异,而不是笼统回答 “有四种”。
  2. 触发时机理解:考察你是否清楚拒绝策略什么时候才会被触发——核心线程满了、队列满了、最大线程数也满了,这个前提条件必须讲明白。
  3. 生产选型能力:能否根据实际业务场景(任务能不能丢、能不能阻塞、需不需要降级)选择合适的策略,甚至自定义策略,这是区分 “背八股文” 和 “真正用过” 的关键。

核心答案

Java 线程池提供了 4 种内置拒绝策略,全部实现自 RejectedExecutionHandler 接口:

拒绝策略 行为 是否抛异常 适用场景
AbortPolicy(默认) 直接抛出 RejectedExecutionException ✅ 是 关键任务,不允许丢失
CallerRunsPolicy 由提交任务的线程自己执行该任务 ❌ 否 不想丢任务,可接受限流
DiscardPolicy 静默丢弃任务,什么都不做 ❌ 否 可容忍任务丢失(如日志)
DiscardOldestPolicy 丢弃队列头部最旧的任务,重新提交当前任务 ❌ 否 优先处理最新任务

一句话总结:生产环境最常用 CallerRunsPolicy(自动限流、不丢任务),关键业务可自定义策略做持久化降级。

深度解析

一、拒绝策略的触发时机

很多面试者上来就背策略,但忽略了 什么时候才会触发 这个关键前提:

  • 核心线程已满:当前运行的线程数已经达到 corePoolSize
  • 任务队列已满:workQueue 已经塞不进新任务了(所以一定要用有界队列,无界队列永远不会满,拒绝策略永远不会触发)
  • 最大线程数已满:线程总数已经达到 maximumPoolSize,无法再创建新线程

关键提醒:如果你用的是 Executors.newFixedThreadPool(),它内部用的是 LinkedBlockingQueue(无界队列),那么队列永远不会满,拒绝策略永远不会触发,任务会一直堆积直到 OOM。这也是为什么生产环境必须用有界队列。

二、四种策略源码解析

每种策略的源码其实非常简单,直接看一眼就懂了:

1. AbortPolicy(默认)

3. 线程数设置多少合适?

面试考察点

  1. 理论基础掌握:面试官不仅仅是想知道一个数字,更是想知道你是否了解 CPU 密集型 和 IO 密集型 的区别,以及背后的理论公式是怎么来的。
  2. 生产实践能力:能否根据实际业务场景灵活调整,而不是死背公式。实际场景往往不是纯 CPU 或纯 IO,而是混合型的,这考验你的实战经验。
  3. 调优思维:是否知道公式只是起点,最终要靠 压测验证 和 线上监控 来确定最优值,体现的是工程化思维。

核心答案

线程数的配置取决于任务的类型,核心判断依据是 线程在执行过程中是否经常需要等待:

任务类型 特征 推荐线程数 举例
CPU 密集型 计算为主,很少等待 N + 1(N = CPU 核心数) 加密解密、压缩、排序、数学计算
IO 密集型 大量等待(网络/磁盘) 2N 或更高 HTTP 调用、DB 查询、文件读写

一句话总结:CPU 密集型设 N+1,IO 密集型设 2N 起步,注意,这只是理论起点,在生产环境中,需要通过性能压测,监控 CPU 利用率、系统负载、上下文切换频率及任务队列长度等指标,进行动态调整,以找到当前场景下的最优解。

深度解析

一、为什么是这两个公式?

先看 核心公式(来自 Java 并发编程之父 Brian Goetz):

的公式是线程数配置的理论基础。核心思想是:线程在等待时 CPU 是空闲的,可以多创建一些线程来充分利用 CPU。

  • CPU 密集型(W/C ≈ 0):线程几乎不等待,一直在计算。这时候创建太多线程反而有害——线程上下文切换的开销比多出来的那点并行收益还大。所以线程数接近 CPU 核心数就好。多给 1 个线程是为了应对偶尔的缺页中断等原因导致的线程暂停,保证 CPU 不空闲。
  • IO 密集型(W/C ≈ 1 或更高):线程大部分时间都在等 IO 响应(等数据库返回、等网络数据),CPU 是闲着的。这时候可以多创建线程,让一个线程等 IO 的时候,另一个线程用 CPU。W/C 越大,说明等待比例越高,可以创建的线程就越多。
二、CPU 密集型:为什么是 N+1 而不是 N?

很多面试者会问,既然 CPU 只有 N 个核心,为什么不是正好 N 个线程?

  • 即使是 CPU 密集型任务,偶尔也会因为 缺页中断(Page Fault)、JVM GC 等原因导致某个线程短暂暂停
  • 这时候 CPU 就有一个核心空闲了,多出来的那个线程刚好可以补上
  • 如果只有 N 个线程,一旦某个线程暂停,CPU 利用率就会短暂下降

但也不能多太多:CPU 密集型场景下,线程远超 CPU 核心数会导致频繁的上下文切换(保存/恢复寄存器、缓存失效等),反而降低性能。

三、IO 密集型:为什么是 2N 或更多?

4. 说说动态线程池实现原理?

面试考察点

  1. 原理理解深度:面试官不仅仅想知道你会不会调 API,更想知道你是否理解线程池内部状态管理的机制,以及动态调整时如何保证线程安全。
  2. 生产实践意识:考察你是否遇到过线程池参数配置不合理导致的问题(如队列堆积、线程不足),以及是否知道如何通过动态调整来在线解决这些问题。
  3. 架构设计能力:动态线程池通常需要结合配置中心(如 Nacos、Apollo)实现,面试官想了解你对 “可观测 + 可控制” 系统设计的理解。

核心答案

动态线程池的核心实现依赖于 ThreadPoolExecutor 提供的 运行时参数修改方法:

方法 作用 说明
setCorePoolSize(int) 动态修改核心线程数 可增可减,减少时会中断多余空闲线程
setMaximumPoolSize(int) 动态修改最大线程数 必须 ≥ 核心线程数
setKeepAliveTime(long, TimeUnit) 动态修改空闲线程存活时间 影响非核心线程回收
setRejectedExecutionHandler(RejectedExecutionHandler) 动态修改拒绝策略 运行时切换策略
allowCoreThreadTimeOut(boolean) 设置核心线程是否可超时回收 默认 false
setThreadFactory(ThreadFactory) 动态修改线程工厂 用于线程命名、优先级等

一句话总结:动态线程池通过 ThreadPoolExecutor 提供的 setter 方法 + 配置中心监听机制,实现运行时参数热更新,核心是 ReentrantLock 保护状态变更 + Worker 线程池的动态伸缩。

深度解析

一、动态线程池整体架构
  1. 配置中心:作为参数的统一存储和管理中心,支持 Nacos、Apollo、Zookeeper 等多种实现。运维人员通过管理界面修改配置,配置中心负责将变更推送到应用端。
  2. 动态配置监听器:应用端通过 ConfigListener 监听配置变化,一旦收到变更通知,立即解析新参数并调用 ThreadPoolExecutor 的 setter 方法进行更新。
  3. ThreadPoolExecutor:Java 原生线程池的核心类,提供了运行时修改参数的能力。内部通过 ReentrantLock 保证状态变更的线程安全。
  4. 监控告警模块:实时监控线程池运行状态(队列积压、活跃线程数、拒绝次数等),当指标异常时触发告警,同时为动态调整提供数据支撑。

整个流程形成了 “监控 → 告警 → 调整 → 生效“ 的闭环,实现线程池的智能化管理。

二、核心方法源码解析

以 setCorePoolSize() 为例,看看 JDK 是如何实现动态调整的:

5. 任务特别多,线程池队列满了怎么办?不能拒绝!

面试考察点

  1. 线程池原理掌握度:面试官不仅仅是想知道你会不会配置线程池,更是想知道你是否理解线程池的执行流程、队列机制以及 4 种拒绝策略的触发时机和各自特点。
  2. 生产问题解决能力:这道题源于真实的生产场景,考察你在面对 “任务不能丢” 这种业务约束时,能否给出多层次的解决方案,而不是只会说 “换更大的队列”。
  3. 系统设计思维:考察你是否具备从线程池层面 → 架构层面 → 中间件层面的多维度思考能力,以及是否理解 “背压(Backpressure)” 机制在高并发系统中的重要性。

核心答案

线程池队列满且不能拒绝任务,需要 分层解决:

层面 方案 核心思路 适用场景
线程池层 CallerRunsPolicy 调用者线程执行,自带背压 中小规模、任务波动
线程池层 动态扩容 调整核心/最大线程数、队列容量 可预测的流量高峰
架构层 消息队列削峰 MQ 异步解耦 + 持久化 高并发、大流量
架构层 任务持久化 + 重试 先存 DB,后台重试提交 任务绝对不能丢
系统层 限流降级 Sentinel 等限流组件 保护系统稳定性

一句话总结:CallerRunsPolicy 是最简单的兜底,消息队列是最可靠的方案,任务持久化是最后的防线。

深度解析

一、线程池拒绝策略回顾

当线程池无法接受新任务时(线程全忙 + 队列已满),会触发拒绝策略:

  1. 核心线程判断:优先检查是否有空闲的核心线程,有则直接执行,这是最快的路径。
  2. 队列入队:核心线程全忙时,任务进入阻塞队列等待。队列是有界的,满则进入下一步。
  3. 非核心线程创建:队列满后,检查是否达到最大线程数,未达则创建非核心线程立即执行。
  4. 拒绝策略触发:线程全忙 + 队列已满 + 无法创建新线程,触发拒绝策略。

关键点在于:拒绝策略触发意味着系统已满载,需要通过合理的策略来应对。

4 种内置拒绝策略:

策略 行为 问题
AbortPolicy 抛异常 ❌ 任务丢失
DiscardPolicy 静默丢弃 ❌ 任务丢失
DiscardOldestPolicy 丢弃队首 ❌ 任务丢失
CallerRunsPolicy 调用者执行 ✅ 不丢任务
二、方案一:CallerRunsPolicy(调用者运行)

这是最简单有效的方案,没有之一:


九、设计模式面试题

共 12 题。

题目清单

  1. 什么是设计模式?它能带来哪些好处?
  2. 设计模式的 7 大基本原则有哪些?
  3. 单例模式有几种写法?
  4. 什么是代理模式?应用场景有哪些?
  5. 什么是责任链模式?应用场景有哪些?
  6. 什么是观察者模式?应用场景有哪些?
  7. 什么是状态模式?应用场景有哪些?
  8. 什么是模板方法模式?应用场景有哪些?
  9. 什么是工厂模式?3种实现方式的区别和特点?
  10. 什么是享元模式?应用场景有哪些?
  11. 策略模式和工厂模式的区别?
  12. 策略模式和 if-else 相比有什么好处?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 什么是设计模式?它能带来哪些好处?

面试考察点

  1. 概念理解:面试官不仅仅是想知道你背没背定义,更是想知道你是否理解设计模式的本质——它是解决特定场景下软件设计问题的通用方案,而不是死板的教条。
  2. 实践经验:考察你在实际项目中是否用过设计模式,能不能结合真实场景说出 “我用了 XX 模式解决了 XX问题”,而不是纸上谈兵。
  3. 设计思维:能否从更高的维度理解设计模式背后的设计原则(SOLID 等),说明你具备良好的架构思维。

核心答案

设计模式(Design Pattern)是在软件开发中,针对反复出现的设计问题所总结出的通用解决方案。它不是可以直接复制的代码,而是一种解决特定问题的思路模板。

这个概念最早由 “四人帮”(GoF,Gang of Four)在 1994 年的著作《设计模式:可复用面向对象软件的基础》中系统提出,总结了 23 种经典设计模式。

设计模式能带来的核心好处:

好处 说明
提高代码复用性 成熟方案可直接套用,避免重复造轮子
增强可维护性 结构清晰的代码更易理解和修改
提升扩展性 遵循 “对扩展开放,对修改关闭” 的原则
降低沟通成本 团队用统一术语交流,”这里用策略模式” 一听就懂
减少设计缺陷 经过大量实践验证,避免踩前人踩过的坑

深度解析

一、23 种设计模式速览

GoF 的 23 种设计模式按用途分为三大类:

  • 创建型:关注对象的创建机制,把对象的创建和使用解耦。比如你不会 new 一个具体的实现类,而是通过工厂来获取。
  • 结构型:关注类和对象的组合方式,像搭积木一样把不同的组件拼装在一起。比如适配器模式让不兼容的接口能协同工作。
  • 行为型:关注对象之间的通信和职责分配,定义 “谁做什么、怎么做”。比如策略模式让算法可以独立变化。
二、设计模式的底层基石——七大设计原则

设计模式不是凭空想出来的,它们都遵循一些基本的设计原则,合称 SOLID 原则(加上迪米特法则和合成复用原则共 7 个):

原则 核心思想 一句话理解
单一职责原则 (SRP) 一个类只做一件事 别把所有功能塞一个类里
开闭原则 (OCP) 对扩展开放,对修改关闭 加功能不改老代码
里氏替换原则 (LSP) 子类可以替换父类 继承要靠谱,别搞特殊
接口隔离原则 (ISP) 接口要小而专 别搞一个万能大接口
依赖倒置原则 (DIP) 依赖抽象而非实现 面向接口编程
迪米特法则 (LoD) 最少知道原则 别打听不需要知道的事
合成复用原则 (CRP) 优先组合而非继承 能组合就别继承

举个实际的例子——策略模式背后的设计原则:

2. 设计模式的 7 大基本原则有哪些?

面试考察点

  1. 基础掌握度:面试官不仅仅是想听你背出 7 个原则的名字,更是想知道你能否准确说出每个原则的核心约束是什么,以及它们之间的优先级关系。
  2. 实践意识:光背定义没用,面试官会追问你 “在实际项目中是怎么体现这些原则的”,考察你是否真正在用这些思想指导编码。
  3. 设计思维:这 7 个原则之间存在相互关联甚至看似矛盾的地方(比如接口隔离和单一职责的区别),能不能把它们当成一个整体来理解,体现的是设计思维的高度。

核心答案

设计模式的 7 大基本原则 如下:

序号 原则名称 英文缩写 一句话概括
1 开闭原则 OCP 对扩展开放,对修改关闭
2 单一职责原则 SRP 一个类只做一个事
3 里氏替换原则 LSP 子类必须能替换父类
4 依赖倒置原则 DIP 面向接口编程,别依赖具体实现
5 接口隔离原则 ISP 接口要小而专,别搞大杂烩
6 迪米特法则 LoD 最少知道原则,少打听闲事
7 合成复用原则 CRP 多用组合,少用继承

其中前 5 个合称 SOLID 原则(取每个缩写的首字母)。开闭原则 是总纲,其他 6 个原则都是为了实现开闭原则的手段。

深度解析

一、原则关系全景图

先看一张关系图,把 7 个原则的定位和关系理清楚:

  • 开闭原则是总目标,它是所有设计原则的 “北极星”,其他原则都是实现它的具体手段
  • 单一职责、里氏替换、依赖倒置 分别从 类的划分、继承的使用、依赖的方向 三个维度来约束设计
  • 接口隔离、迪米特法则、合成复用 分别从 接口的粒度、对象间交互、复用方式 三个维度进一步细化约束

理解了这张关系图,你就不会觉得 7 个原则是一盘散沙了。

二、逐个击破
1. 开闭原则(OCP)—— 总纲

对扩展开放,对修改关闭。

这是最核心的原则。什么意思?当需求变化时,你应该通过新增代码来扩展功能,而不是修改已有代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 反例:每次新增折扣类型都要改这个方法(修改老代码 💣)
public class DiscountCalculator {
public double calculate(String type, double price) {
if ("VIP".equals(type)) {
return price * 0.8;
} else if ("SVIP".equals(type)) {
return price * 0.7;
}
// 新加一种会员等级?又得改这里...
return price;
}
}

// 正例:通过扩展来实现,不修改老代码 ✅
public interface DiscountStrategy {
double calculate(double price);
}

public class VipDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.8;
}
}

public class SvipDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.7;
}
}

// 新加会员等级?写个新类就行,老代码一行不动

3. 单例模式有几种写法?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你背了几种写法,更是想看你能否说出每种写法的优缺点和适用场景,而不是只会一种 “背答案”。
  2. 多线程安全意识:单例模式最核心的考点就是线程安全。面试官想看你是否理解并发环境下的问题,以及如何优雅地解决——这才是拉开差距的地方。
  3. JVM 原理深度:能否讲清楚 volatile 的作用、类加载机制、序列化破坏单例等底层原理,是区分 “会用” 和 “真正懂” 的关键。

核心答案

先说结论:单例模式主要有 5 种经典写法——

写法 线程安全 延迟加载 推荐指数
饿汉式 ✅ 安全 ❌ 否 ⭐⭐⭐
懒汉式(同步方法) ✅ 安全 ✅ 是 ⭐
双重检查锁(DCL) ✅ 安全 ✅ 是 ⭐⭐⭐⭐
静态内部类 ✅ 安全 ✅ 是 ⭐⭐⭐⭐⭐
枚举单例 ✅ 安全 ❌ 否 ⭐⭐⭐⭐⭐

一句话总结:一般场景推荐静态内部类,需要防反射和序列化破坏时用枚举单例。

深度解析

一、饿汉式

类加载时就创建实例,简单粗暴,天生线程安全。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
// 类加载时就初始化
private static final Singleton INSTANCE = new Singleton();

// 私有构造器,防止外部 new
private Singleton() {}

public static Singleton getInstance() {
return INSTANCE;
}
}

优点是写法简单,线程安全(JVM 保证类加载过程的线程安全性)。缺点也很明显——不管用不用都先创建,浪费资源。如果你的单例对象很大但未必会用到,这就不合适了。

二、懒汉式(同步方法)

用到的时候才创建,但为了线程安全给整个方法加了 synchronized。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static Singleton instance;

private Singleton() {}

// 整个方法加锁,性能堪忧
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

说实话,这种写法在面试里就是用来 “踩” 的。虽然线程安全,但每次调用 getInstance() 都要获取锁,而实际上只有第一次创建时才需要同步。这在高并发场景下性能损耗非常大,生产环境基本不用。

三、双重检查锁(DCL)

这个是面试的 “重头戏”,也是面试官最爱追问的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {
// 注意:volatile 很关键,不能省!
private static volatile Singleton instance;

private Singleton() {}

public static Singleton getInstance() {
// 第一次检查,避免不必要的加锁
if (instance == null) {
synchronized (Singleton.class) {
// 第二次检查,防止多个线程同时通过第一次检查
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

4. 什么是代理模式?应用场景有哪些?

面试考察点

  1. 概念理解:面试官不仅仅是想知道代理模式的定义,更是想知道你是否理解它的核心思想——“不直接访问目标对象,通过代理间接访问”,以及为什么要这么设计。
  2. 技术深度:能否区分静态代理和动态代理,能否讲清楚 JDK 动态代理和 CGLIB 的底层原理差异。这块能答好的候选人不多,答好了就是加分项。
  3. 框架关联:面试官特别想看你是否能把代理模式和 Spring AOP、MyBatis Mapper、RPC 远程调用等实际框架联系起来,证明你不止停留在理论层面。

核心答案

一句话定义:代理模式就是给目标对象提供一个 “替身”,由替身控制对目标对象的访问。客户端不直接和目标对象打交道,而是通过代理对象间接访问。

打个生活中的比方:你要租房,不直接找房东,而是找中介。中介就是 “代理”,房东就是 “目标对象”。中介可以在租房前后加一些额外操作(比如带看房、签合同、收中介费),而你只需要和中介打交道就行。

上面的图展示了代理模式的核心结构。关键角色有三个:

  • Subject(公共接口):定义了代理和目标对象的共同行为,客户端面向接口编程
  • RealSubject(目标对象):真正干活的那个,也就是 “房东”
  • Proxy(代理对象):持有目标对象的引用,在调用目标方法前后可以增加额外逻辑,也就是 “中介”

核心思路就是:代理和目标对象实现同一个接口,代理内部持有目标的引用,客户端只跟代理打交道。

深度解析

一、静态代理

静态代理的 “静态” 体现在:代理类在编译期就确定了,需要手动编写代理类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 公共接口
public interface UserService {
void addUser(String name);
}

// 目标对象
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
}

// 代理对象(手动编写)
public class UserServiceProxy implements UserService {

private UserService target; // 持有目标对象的引用

public UserServiceProxy(UserService target) {
this.target = target;
}

@Override
public void addUser(String name) {
System.out.println("[日志] 开始添加用户..."); // 前置增强
target.addUser(name); // 调用目标方法
System.out.println("[日志] 添加用户完成"); // 后置增强
}
}

// 使用
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.addUser("犬小哈");

十、Spring 面试题

共 22 题。

题目清单

  1. SpringBoot 和 Spring 的区别是什么?
  2. Spring 中创建 Bean 有几种方式?
  3. Spring 中的 Bean 是线程安全的吗?
  4. Spring 中 Bean 的作用域有哪些?
  5. @Autowired 和 @Resource 注解的区别?
  6. Spring Bean 的生命周期是怎么样的?
  7. 谈谈 Spring 的 AOP?
  8. 谈谈 Spring 的 IOC?
  9. Spring 的 AOP 在什么场景下会失效?
  10. Spring 中用到了哪些设计模式?
  11. BeanFactory 和 FactroyBean 的关系?
  12. 什么是 Spring 的三级缓存?
  13. 什么是 Spring 的循环依赖问题?
  14. Spring 的事务传播机制有哪些?
  15. Spring 事务失效可能是哪些原因?
  16. 如何在 Spring 启动过程中做缓存预热?
  17. 什么是 Spring MVC 三层架构?
  18. SpringBoot 的启动流程是怎样的?
  19. SpringBoot 如何实现 main 方法启动 Web 容器的?
  20. SpringBoot 自动配置是如何实现的?
  21. SpringMVC 处理请求的流程是怎样的?
  22. Spring Boot Controller 层怎么实现并发安全的?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. SpringBoot 和 Spring 的区别是什么?

面试考察点

  1. 认知深度:面试官不仅仅是想知道 “SpringBoot 更方便” 这个表层结论,更是想看你能否讲清楚 SpringBoot 到底帮你自动做了哪些事,以及背后的原理(自动配置、起步依赖等)。
  2. 历史演进理解:能否说出 Spring 时代的开发痛点(配置地狱、版本冲突、手动部署),以及 SpringBoot 如何逐一解决这些痛点。
  3. 底层原理:能否讲清楚 @SpringBootApplication 注解背后的三个核心注解、spring.factories 自动配置的加载机制、内嵌 Tomcat 的启动流程。

核心答案

**先说结论:SpringBoot 不是 Spring 的替代品,而是 Spring 的 “快速开发脚手架”**。它底层 100% 依赖 Spring 框架,只是在 Spring 之上做了一层 “约定优于配置” 的封装,让你开箱即用。

对比维度 Spring SpringBoot
核心定位 企业级 Java 开发框架 Spring 的快速开发脚手架
配置方式 大量 XML / Java 配置 约定优于配置,零 XML
依赖管理 手动管理版本,容易冲突 起步依赖(Starter),自动管理版本
内嵌服务器 无,需手动部署 WAR 到外部容器 内嵌 Tomcat/Jetty/Undertow,java -jar 直接运行
自动配置 无,手动配 Bean 根据类路径自动装配(@EnableAutoConfiguration)
生产监控 无内置 Actuator 提供健康检查、指标监控
启动方式 部署到 Servlet 容器 独立运行,main() 方法启动

一句话总结:Spring 是引擎,SpringBoot 是带自动挡的车。引擎还是那个引擎,但不用你手动挂挡了。

深度解析

一、从 Spring 的痛点说起

没用过纯 Spring 的人可能不知道当年有多痛苦。我最早用 Spring MVC 做项目的时候,一个 web.xml 加 applicationContext.xml 能写到几百行。

Spring 时代的典型开发流程:

上面的图展示了纯 Spring 开发的典型痛点:

  • 依赖版本冲突:Spring 各模块版本要自己管理,不同模块之间的版本兼容性容易出问题
  • 配置地狱:光配置文件就几百行,而且每个项目都差不多,大量重复劳动
  • 手动配置组件:数据源、事务管理器、视图解析器这些基础组件每次都要手动配
  • 部署繁琐:打 WAR 包、部署到外部 Tomcat、处理端口冲突和类冲突

SpringBoot 的出现就是为了解决这些痛点。

二、SpringBoot 的三大核心特性
特性 1:起步依赖(Starter)

2. Spring 中创建 Bean 有几种方式?

面试考察点

  1. 全面性:面试官不仅仅是想知道 “注解 + XML” 两种方式,更是想看你能否系统地列出所有注册 Bean 的方式,包括 @Bean、@Component、FactoryBean、@Import 等容易遗漏的方式。
  2. 原理深度:能否讲清楚每种方式底层走的是哪条创建路径(反射、工厂方法、代理),以及它们的执行时机。
  3. 实战区分:能否说清楚 @Bean 和 @Component 的区别,FactoryBean 和 BeanFactory 的区别。这两个对比追问几乎是标配。

核心答案

先说结论:Spring 中注册 Bean 到容器主要有 6 种方式。

方式 关键字/注解 适用场景 常用程度
组件扫描 @Component / @Service / @Repository / @Controller 自己写的类,直接标注 ⭐⭐⭐⭐⭐
配置类方法 @Configuration + @Bean 第三方库的对象 ⭐⭐⭐⭐⭐
FactoryBean 实现 FactoryBean<T> 接口 创建逻辑复杂的 Bean ⭐⭐⭐
@Import @Import / ImportSelector / ImportBeanDefinitionRegistrar 动态/条件注册 Bean ⭐⭐⭐
XML 配置 <bean> 标签 老项目维护 ⭐⭐
编程式注册 BeanDefinitionRegistry 框架开发、动态注册 ⭐⭐

实际开发中最常用的是前两种:@Component 系列注解用于自己的类,@Bean 用于第三方库。

深度解析

一、@Component 系列注解(组件扫描)

这是最常见的方式。SpringBoot 启动时会自动扫描 @SpringBootApplication 所在包及其子包下所有标注了 @Component(及其衍生注解)的类,自动注册为 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class UserService {
// 自动注册为 Bean,beanName 默认为 "userService"
}

@Repository
public class UserDao {
// @Repository 也能注册,语义上区分数据访问层
}

@Controller
public class UserController {
// @Controller 用于 Web 层
}

@Component
public class MyUtils {
// 通用组件
}

@Service、@Repository、@Controller 本质上就是 @Component 的别名,功能完全一样,只是语义不同。面试官有时会追问 “这四个注解有什么区别”,答案就是:运行时没有区别,纯粹是语义区分。

3. Spring 中的 Bean 是线程安全的吗?

面试考察点

  1. 作用域理解:面试官不仅仅是想知道 “安全还是不安全” 这个二选一的答案,更想考察你是否清楚 Spring Bean 的不同作用域(Singleton、Prototype 等),以及作用域如何影响线程安全。
  2. 线程安全意识:考察你是否理解多线程环境下共享变量的风险,能不能区分 “有状态” 和 “无状态” Bean,以及在实际开发中如何规避线程安全问题。
  3. 框架原理深度:如果你能说出 Spring 并没有对 Bean 做线程安全的保障,而是把责任交给了开发者,说明你对 Spring 容器的定位理解到位。

核心答案

直接说结论:Spring 中的 Bean 默认不是线程安全的。

准确地说,Spring 容器本身并不提供 Bean 的线程安全保证。线程安全与否,取决于 Bean 的 作用域 和 是否有状态。

作用域 说明 线程安全性
Singleton(默认) 整个容器只有一个实例 ⚠️ 不安全(多线程共享)
Prototype 每次获取都创建新实例 ✅ 相对安全(各线程独立)
Request 每个 HTTP 请求一个实例 ✅ 安全(单线程内使用)
Session 每个 HTTP Session 一个实例 ⚠️ 可能不安全(同 Session 多请求并发)

绝大多数 Bean 都是 Singleton 作用域,多个线程会同时访问同一个实例,如果这个 Bean 里有可变的成员变量,那就是线程不安全的。

深度解析

一、为什么 Singleton Bean 不安全?

先看一个踩坑示例:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class UserService {

// 危险!这是可变的成员变量
private String currentUser;

public void processUser(String username) {
this.currentUser = username; // 线程 A 设置了 "张三"
// 线程 B 可能在此时把 currentUser 改成了 "李四"
doSomething(); // 线程 A 读到的可能是 "李四"!
}
}

清楚地展示了问题所在:Singleton 作用域下,Spring 容器里只有一个 UserService 实例,所有线程共享它。一旦某个线程修改了成员变量 currentUser,其他线程读到的就是脏数据。

  • 根因:多个线程同时操作同一个对象的可变成员变量,没有任何同步机制保护
  • 表现:数据错乱、脏读、甚至 NullPointerException(对象被部分修改)
二、那为什么我们平时写的 Bean 没出问题?

因为实际开发中,我们写的 Bean 大多是 无状态 的。

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class UserService {

@Autowired
private UserMapper userMapper; // 注入的依赖,本身也是无状态的

public User getUserById(Long id) {
// 没有可变的成员变量,只有局部变量和参数
// 局部变量存在线程私有的栈中,天然线程安全
return userMapper.selectById(id);
}
}

4. Spring 中 Bean 的作用域有哪些?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你能背出几种作用域,更是想看你能否准确描述每种作用域的生命周期和适用场景,能不能说出 singleton 和 prototype 的核心区别。
  2. 实践经验:考察你是否在真实项目中用过非 singleton 的作用域,比如 Web 环境下的 request、session 作用域,有没有踩过坑。
  3. 原理理解:是否理解 singleton 下注入 prototype Bean 的 “陷阱”,以及解决方案,这才是拉开差距的地方。

核心答案

Spring 框架支持 6 种 Bean 作用域,其中前 2 种是所有环境都可用的,后 4 种只在 Web 环境下生效——

作用域 说明 适用环境
singleton 默认值。整个 IoC 容器中只有一个 Bean 实例 所有环境
prototype 每次获取 Bean 时都创建一个新实例 所有环境
request 每个 HTTP 请求创建一个实例,请求结束即销毁 仅 Web
session 每个 HTTP Session 创建一个实例,Session 结束即销毁 仅 Web
application 每个 ServletContext 对应一个实例 仅 Web
websocket 每个 WebSocket 会话对应一个实例 仅 Web

深度解析

一、singleton — 默认但别无脑用

singleton 是 Spring 的默认作用域,意思是 Spring 容器启动时(严格来说是首次被请求时)创建一个实例,之后所有地方拿到的都是同一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
// 默认就是 singleton,写不写都一样
@Component
public class UserService {
// ...
}

// 显式指定
@Component
@Scope("singleton")
public class UserService {
// ...
}

大多数业务场景用 singleton 没问题,因为 Spring Bean 通常是无状态的(不保存会话数据),靠方法参数和 ThreadLocal 来处理并发。但如果你在 singleton Bean 里存了可变状态,那就是给自己埋雷——多线程并发访问会出问题。

二、prototype — 用得少但得懂

prototype 作用域下,每次从容器中获取 Bean(通过 getBean() 或依赖注入),都会拿到一个全新的实例。Spring 只负责创建,不负责销毁,生命周期管理交给了使用者。

1
2
3
4
5
6
7
8
9
@Component
@Scope("prototype")
public class PaymentTask {
private String orderId;

public void setOrderId(String orderId) {
this.orderId = orderId;
}
}

5. @Autowired 和 @Resource 注解的区别?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道这两个注解 “不一样”,更是想看你能不能从来源、匹配策略、使用方式等多个维度做系统对比,而不是只背出 “一个是 Spring 的,一个是 JDK 的”。
  2. 底层原理:是否理解 @Autowired 的 “先类型后名称” 和 @Resource 的 “先名称后类型” 的匹配机制,以及当存在多个候选 Bean 时各自怎么处理冲突。
  3. 实践选型:考察你在项目中如何选择这两个注解,是否了解 Spring 官方的推荐做法,以及新版 Spring Boot 中的变化。

核心答案

先上对比表格,一目了然:

维度 @Autowired @Resource
来源 Spring 框架(org.springframework.beans.factory.annotation) JSR-250 规范(jakarta.annotation / javax.annotation)
匹配策略 先按类型(byType),有多个再按名称 先按名称(byName),没找到再按类型
指定名称 配合 @Qualifier("beanName") 直接 @Resource(name = "beanName")
required 属性 支持 required = false,注入失败不报错 不支持,注入失败直接抛异常
适用位置 构造器、方法、参数、字段 方法、字段
官方推荐 Spring 官方推荐 —

核心区别就一句话:**@Autowired 先类型后名称,@Resource 先名称后类型**。

深度解析

一、匹配机制的差异

这是面试官最想听的部分。用一张流程图来对比两者的注入策略:

两个流程图分别展示了两种注解的注入策略,关键区别在于优先级不同:

  • @Autowired 的策略:Spring 先按类型去容器里找,如果只找到一个,直接注入,皆大欢喜。但如果找到多个同类型的 Bean,Spring 会尝试用字段名去匹配 Bean 名称,匹配上了就用。如果还匹配不上,就得靠 @Qualifier 显式指定。另外 @Autowired 支持 required = false,允许注入失败时不报错,这在某些可选依赖的场景很有用。
  • @Resource 的策略:正好反过来。它先看有没有指定 name,或者直接用字段名当 Bean 名称去容器里找。找到了就注入,找不到才退回按类型查找。但 @Resource 没有 required 属性,找不到就报错,没有商量的余地。
二、当存在多个同类型 Bean 时怎么办

这是实际开发中经常遇到的场景。假设你有两个 UserService 的实现:


十一、Tomcat 面试题

共 5 题。

题目清单

  1. 过滤器和拦截器的区别是什么?
  2. 介绍一下 Tomcat 的 IO 模型?
  3. Tomcat 的类加载机制是怎样的?
  4. Tomcat 中有哪些类加载器?
  5. 为什么 Tomcat 默认最大线程数是 200,而不是 N+1?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 过滤器和拦截器的区别是什么?

面试考察点

  1. 架构理解:面试官不仅仅是想知道两者的定义,更是想看你能否说清楚它们在请求处理链中的位置关系,以及为什么 Spring 要在 Filter 之外再设计一套 Interceptor。
  2. 实战经验:是否在项目中真正用过,能否举出合适的场景说明什么时候该用 Filter、什么时候该用 Interceptor,而不是笼统地说 “都能做”。
  3. 原理深度:能否从 Servlet 容器、Spring IoC、Spring MVC 的 DispatcherServlet 等层面解释两者的本质差异。

核心答案

一句话先给结论:过滤器(Filter)是 Servlet 规范的一部分,由 Servlet 容器管理;拦截器(Interceptor)是 Spring MVC 框架的一部分,由 Spring 容器管理。 两者都能在请求到达 Controller 前后做处理,但作用范围、生命周期、能获取的信息量完全不同。

核心区别一览表:

对比维度 Filter(过滤器) Interceptor(拦截器)
规范归属 Servlet 规范 Spring MVC 框架
管理容器 Servlet 容器(Tomcat) Spring IoC 容器
作用范围 所有请求(包括静态资源) 仅 DispatcherServlet 处理的请求
触发时机 在 DispatcherServlet 之前 在 DispatcherServlet 之内,Handler 执行前后
能否获取 Handler 信息 不能 能(方法名、参数、注解等)
能否注入 Spring Bean 默认不能(需额外配置) 天然支持
典型场景 编码转换、跨域、压缩、全局通用 权限校验、日志、事务、业务相关

深度解析

一、请求处理链路

光看表格还不够直观,咱们来画一下一次 HTTP 请求的完整链路:

  • Filter 层(Servlet 容器层面):所有请求先进 Filter 链,Filter 不知道后面是 Spring 还是其他框架,它只认 Servlet。Filter 的执行顺序由注册顺序或 @Order 决定。
  • DispatcherServlet 层(Spring MVC 入口):请求穿过 Filter 链后到达 DispatcherServlet,从这里开始进入 Spring 的地盘。DispatcherServlet 负责根据 URL 找到对应的 Handler(即 Controller 方法)。
  • Interceptor 层(Spring MVC 框架层面):找到 Handler 之后、真正执行之前,先依次执行所有 Interceptor 的 preHandle() 方法;Controller 执行完毕后,逆序执行 postHandle();视图渲染完成后,再逆序执行 afterCompletion()。

2. 介绍一下 Tomcat 的 IO 模型?

面试考察点

  1. IO 基础功底:面试官不仅仅是想知道 Tomcat 支持哪些 IO 模型,更是想看你对 BIO、NIO、AIO 这些底层概念是不是真的理解了,还是只会背个名字。很多人答 “Tomcat 用的是 NIO”,但问他 NIO 具体怎么做到非阻塞的,就卡壳了。
  2. Tomcat 架构认知:是否清楚 Tomcat 从 6 到 10 这几个大版本中 IO 模型的演进路线,为什么 BIO 被淘汰、APR 是怎么提升性能的。
  3. 生产实践:能不能根据实际业务场景选择合适的 IO 模型和 Connector 配置,而不是 “默认就好”。

核心答案

Tomcat 支持 3 种 IO 模型,分别对应三种 ProtocolHandler 实现:

IO 模型 ProtocolHandler 核心原理 性能表现
BIO Http11Protocol 阻塞式 IO,一个连接一个线程 低(已废弃)
NIO Http11NioProtocol 基于 Java NIO,多路复用 高
APR Http11AprProtocol 基于 Apache 可移植运行时库,调用操作系统原生 API 最高

从 Tomcat 8.5 开始,BIO 已被正式移除,NIO 成为默认选项。Tomcat 9+ 还支持 NIO.2(Http11Nio2Protocol),底层用的是 JDK 7 引入的异步 IO(AIO)。

生产环境的话,能装 APR 就装 APR,装不了就用 NIO,基本也够用了。

深度解析

一、BIO——一个连接一个线程,说拜拜了

BIO 是最原始的模型。工作方式非常简单粗暴:每个进来的 TCP 连接,Tomcat 都会分配一个独立线程来处理,线程在读写数据时是阻塞的。

  • 一个连接对应一个线程:每当有新的 TCP 连接进来,Tomcat 的 Acceptor 线程接受连接后,会把该连接交给一个独立的工作线程(从线程池取或新建)。这个工作线程负责完成 HTTP 请求的读取、Servlet 调用、响应写回整个过程。
  • 全程阻塞:工作线程在读请求体、写响应的时候都是阻塞的。如果客户端网络慢、发送数据慢,线程就一直等着,啥也干不了。
  • 致命问题:假设你有 1000 个并发连接(很多可能只是 “挂着” 但没发数据),就需要 1000 个线程。每个线程栈默认 1MB,光线程栈就吃掉 1GB 内存。而且线程上下文切换的开销也非常大。

这就是为什么 Tomcat 8.5 把 BIO 干掉了——高并发场景下实在扛不住。

二、NIO——多路复用,Tomcat 的默认选择

NIO 是目前 Tomcat 的默认 IO 模型,也是面试官最想听你展开讲的部分。

核心思想:不再一个连接一个线程,而是用一个线程通过 Selector 轮询多个连接,哪个连接有数据可读了再去处理。

3. Tomcat 的类加载机制是怎样的?

面试考察点

  1. JVM 类加载基础:面试官不仅仅是想知道 Tomcat 的类加载器长什么样,更是想确认你是否真正理解双亲委派模型——它的好处是什么、限制又是什么。基础不牢的话,这道题很难答到点子上。
  2. Tomcat 特殊需求理解:Tomcat 作为一个 Web 容器,要同时跑多个 Web 应用,它面临的问题(应用隔离、类库共享、热部署)普通 JVM 根本不用操心。你能不能把这些需求讲清楚,是这道题的加分关键。
  3. 设计取舍的判断力:Tomcat “破坏” 双亲委派不是瞎搞,而是有意为之。面试官想看你是否理解 “什么时候该遵守规则、什么时候该打破规则” 这种架构决策能力。

核心答案

先说结论:Tomcat 没有完全遵守 JVM 的双亲委派模型,而是设计了一套自己的类加载器体系,核心目的是实现 Web 应用之间的类隔离和类库共享。

JVM 默认的双亲委派模型要求类加载请求一路向上委托给父加载器,父加载器找不到才由自己加载。但 Tomcat 有几个刚需双亲委派满足不了:

  • 应用隔离:两个 Web 应用都依赖了 commons-lang3,但版本不同(一个是 3.12,一个是 3.14),必须互不干扰
  • 类库共享:Spring、日志框架这些公共类库,所有 Web 应用共享一份就行,没必要每个应用都加载一份
  • 热部署:重新部署一个 Web 应用时,需要把旧的类加载器扔掉,创建一个新的,而不影响其他应用

所以 Tomcat 的做法是:该共享的共享,该隔离的隔离,灵活运用但不乱来。

深度解析

一、Tomcat 类加载器体系全景图
  • JVM 标准层(Bootstrap → Extension → Application):这三层就是 JVM 默认的双亲委派,Tomcat 没动它。Bootstrap 加载 JDK 核心类库(rt.jar),Extension 加载扩展类库,Application 加载 Tomcat 自身的启动类(比如 catalina.sh 跑起来的入口类)。
  • Tomcat 公共层(Common ClassLoader):这是 Tomcat 自定义的第一个类加载器,负责加载 $CATALINA_HOME/lib 目录下的所有 jar 包。像 servlet-api.jar、el-api.jar 以及你放到 lib 目录下的公共类库(比如 Spring、日志框架),都由它来加载。所有 Web 应用共享这一层。
  • Web 应用隔离层(WebApp ClassLoader):这是整个体系的核心。 每个 Web 应用都有一个独立的 WebAppClassLoader,负责加载 WEB-INF/classes 和 WEB-INF/lib 下的类。应用 A 和应用 B 各有自己的 WebAppClassLoader,互相看不到对方的类,实现了隔离。

十二、Zookeeper 面试题

共 9 题。

题目清单

  1. Zookeeper 是干什么的?
  2. Zookeeper 是 CP 的还是 AP 的?
  3. Zookeeper 的典型应用场景有哪些?
  4. Zookeeper 的数据结构是怎样的?
  5. Zookeeper 的 watch 机制是如何工作的?
  6. 如何通过 Zookeeper 实现服务注册与发现?
  7. 如何用 Zookeeper 实现分布式锁?
  8. Zookeeper 脑裂是什么?怎么解决?
  9. Zookeeper 是如何保证创建的节点是唯一的?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. Zookeeper 是干什么的?

面试考察点

  1. 分布式基础认知:面试官不仅仅是想知道 Zookeeper 是什么,更是想考察你是否理解分布式系统中的核心问题——协调、一致性、元数据管理。如果连 Zookeeper 解决什么问题都说不清楚,那分布式这块基本就凉了。
  2. 实际应用场景:考察你是否知道 Zookeeper 在工业界的实际用途(注册中心、配置中心、分布式锁、Leader 选举等),而不只是停留在概念层面。
  3. 原理理解深度:如果你提到 Zookeeper 能保证一致性,面试官会继续追问 ZAB 协议、Watcher 机制、Session 机制等,看你是 “会用” 还是 “懂原理”。

核心答案

一句话:Zookeeper 是一个分布式的、高可用的协调服务,本质上是一个树形结构的文件系统 + 事件监听机制,用来解决分布式系统中的协调问题。

它提供了 4 大核心能力:

能力 一句话解释 典型场景
数据存储 树形命名空间,每个节点(ZNode)可以存数据 配置中心、元数据管理
监听通知 客户端可以监听节点的变化,变化时收到通知 服务上下线感知、配置热更新
集群管理 维护集群节点的状态信息 服务注册与发现
一致性保证 通过 ZAB 协议保证各节点数据一致 分布式锁、Leader 选举

常见的应用场景:Dubbo/Nacos 的注册中心(早期)、Kafka 集群管理、Hadoop/HBase 的 Master 选举、分布式锁实现等。

深度解析

一、Zookeeper 的数据模型:树形命名空间

Zookeeper 的数据结构长得像文件系统,但每个 “文件”(ZNode)既能存数据,又能挂子节点。

  • 持久节点(Persistent):创建后一直存在,除非主动删除。适合存配置信息
  • 临时节点(Ephemeral):和客户端的 Session 绑定,Session 断开节点自动删除。这是实现服务注册和分布式锁的关键
  • 顺序节点(Sequential):创建时自动在名称后面追加递增序号,比如 /lock-0000000001。分布式锁的核心依赖
  • 每个 ZNode 默认最多存 1MB 数据,所以 Zookeeper 不适合存大数据,它存的是 “元数据”(配置、地址、状态等)
二、Zookeeper 怎么实现 “分布式锁”?

这是面试中被问得最多的场景。核心思路:利用临时顺序节点 + Watch 机制。

  • 每个客户端在 /locks 下创建一个临时顺序节点
  • 序号最小的那个客户端获得锁
  • 没拿到锁的客户端只监听自己前一个节点(而不是所有节点),这样当前一个节点被删除时,只有下一个客户端被唤醒
  • 这就是所谓的 “公平锁”——先来先得,按顺序排队
三、Zookeeper 怎么实现 “服务注册与发现”?

以 Dubbo 为例,服务提供者和消费者都往 Zookeeper 里注册信息:

2. Zookeeper 是 CP 的还是 AP 的?

面试考察点

  1. CAP 理论理解:面试官不仅仅是想知道 Zookeeper 属于 CP 还是 AP,更是想考察你是否真正理解 CAP 三要素的含义,以及为什么分布式系统不能同时满足三者。
  2. Zookeeper 的一致性机制:考察你是否知道 Zookeeper 通过 ZAB 协议保证了强一致性,以及在 Leader 选举期间会发生什么。
  3. 实际取舍分析:考察你是否能结合实际场景分析 Zookeeper 牺牲可用性的具体表现,而不是只会背结论。

核心答案

Zookeeper 是 CP 系统。

它优先保证一致性和分区容错性,在发生网络分区或 Leader 选举期间,会牺牲可用性——集群在这段时间内无法处理写请求(甚至部分读请求也可能返回旧数据)。

用一张表快速对比常见中间件的 CAP 归属:

中间件 CAP 归属 牺牲了什么 具体表现
Zookeeper CP 可用性 A Leader 选举期间集群不可用
Eureka AP 强一致性 C 节点间数据可能不一致,但始终可用
Nacos CP / AP 可切换 灵活选择 临时实例走 AP,永久实例走 CP
etcd CP 可用性 A 和 Zookeeper 类似,Leader 选举期间不可写
Consul CP 可用性 A Raft 协议,Leader 选举期间不可写

深度解析

一、先搞清楚 CAP 到底在说什么

很多人背了无数遍 CAP,但一问细节就糊。先把基础夯实:

  • 在分布式系统中,网络分区(P)是客观存在的,不是你能选的
  • 你能选的是:当分区发生时,你是要一致性(C)还是要可用性(A)
  • CP 系统的立场:数据不一致宁可挂掉也不能返回错误数据
  • AP 系统的立场:哪怕数据暂时不一致,也得给用户响应,不能让人干等着
二、Zookeeper 为什么是 CP?具体怎么体现?

分两个层面来理解:数据写入时的一致性保证 和 异常时的可用性牺牲。

层面一:正常情况下——ZAB 协议保证强一致性

  • Leader 收到写请求后,不是自己写完就返回,而是向所有 Follower 发起 Proposal
  • 必须等 超过半数 的 Follower 写入本地日志并返回 ACK,Leader 才会 Commit
  • 这意味着:任意时刻,已提交的数据在多数节点上一定是一致的

层面二:Leader 选举期间——牺牲可用性

这是 Zookeeper 作为 CP 系统最典型的体现:

  • Leader 挂了之后,整个集群进入选举模式
  • 选举期间,所有写请求全部被阻塞,不响应
  • 直到新 Leader 选出来,数据同步完毕,才能恢复服务
  • 这就是典型的 “宁可不可用,也不能返回不一致的数据”
三、一个常见的争议:Zookeeper 的读请求到底算不算强一致?

这个点很多人搞不清楚,值得展开说一下。

3. Zookeeper 的典型应用场景有哪些?

面试考察点

  1. 核心能力理解:面试官不仅仅是想知道你背了几个场景名,更是想知道你是否理解 ZooKeeper 的底层能力(如临时节点、顺序节点、Watch 机制)是如何支撑这些场景的。
  2. 实践经验:你是否在实际项目中用过 ZooKeeper?是直接用原生 API 还是基于 Curator 框架?踩过什么坑?
  3. 技术选型意识:了解 ZooKeeper 的适用边界,知道哪些场景该用、哪些场景现在有更好的替代方案。

核心答案

ZooKeeper 的典型应用场景主要有 5 大类:

应用场景 核心原理 典型使用方
分布式锁 临时顺序节点 + Watch 需要互斥访问的业务
服务注册与发现 临时节点 + Watch Dubbo、Kafka(旧版)
分布式配置中心 数据节点 + Watch 统一配置管理
分布式协调/通知 Watcher 机制 分布式事务、任务分配
分布式屏障 顺序节点 + Watch 并行任务汇合点

这 5 个场景的底层其实就依赖 ZooKeeper 的三大核心能力:临时节点(会话结束自动删除)、顺序节点(自带递增编号)、Watch 机制(变更实时通知)。理解了这仨,上面的场景都是组合运用。

深度解析

一、服务注册与发现

这个应该是大家最熟悉的场景了。Dubbo 默认就用 ZooKeeper 做注册中心。

  • Provider 注册:服务提供者启动时,在 /services/order 下创建一个临时节点,节点数据保存自己的 IP 和端口信息。
  • Consumer 订阅:服务消费者启动时,读取 /services/order 下所有子节点,获取可用的 Provider 列表,同时在这些节点上注册 Watch。
  • 自动摘除:Provider 宕机后,会话失效,临时节点自动删除,Consumer 通过 Watch 收到通知,更新本地服务列表。

为什么用临时节点?因为临时节点跟会话绑定,Provider 挂了之后节点自动消失,不需要手动做下线操作。这个设计确实优雅。

二、分布式锁

分布式锁是面试追问频率最高的场景。核心思路:利用 ZooKeeper 的临时顺序节点实现公平锁。

  • 创建节点:每个客户端在 /lock 下创建临时顺序节点(如 node-000001、node-000002)。
  • 判断顺序:获取 /lock 下所有子节点,判断自己是否为序号最小的那个。最小则获得锁。
  • 等待通知:如果不是最小的,就 Watch 自己前一个节点(注意,不是 Watch 所有节点,这叫”羊群效应”的优化)。
  • 释放锁:客户端完成操作后删除自己的节点,或者会话断开后临时节点自动删除,下一个客户端收到通知。

用临时节点而不是持久节点的好处是:客户端崩溃后锁会自动释放,不会出现死锁。这个设计比基于数据库的分布式锁靠谱多了。


十三、RabbitMQ 面试题

共 9 题。

题目清单

  1. RabbitMQ 的整体架构是怎样的?
  2. RabbitMQ 是如何保证高可用的?
  3. RabbitMQ 如何防止重复消费?
  4. RabbitMQ 如何保证消息不丢失?
  5. RabbitMQ 怎么实现延迟消息?
  6. 什么是 RabbitMQ 的死信队列?
  7. 说说 RabbitMQ 的事务机制?
  8. RabbitMQ 如何实现消费端限流?
  9. 怎么保证消息一定能发送到 RabbitMQ?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. RabbitMQ 的整体架构是怎样的?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你有没有用过 RabbitMQ,更是想确认你是否清楚它的核心组件(Producer、Broker、Exchange、Queue、Consumer、Binding)以及它们之间的关系。
  2. 原理理解深度:能否完整描述一条消息从生产到消费的全链路,是否理解 Exchange 的路由机制和 Binding 的匹配规则。
  3. 架构认知:是否了解 RabbitMQ 集群架构(普通集群 vs 镜像队列 vs 仲裁队列),以及 HAProxy + Keepalived 等高可用方案。

核心答案

RabbitMQ 的整体架构可以用一句话概括:生产者不直接把消息发给队列,而是发给 Exchange,由 Exchange 根据路由规则将消息投递到对应的队列,消费者再从队列中拉取消息。

核心组件一览:

组件 作用 类比
Producer 发送消息的应用 寄件人
Consumer 接收消息的应用 收件人
Broker RabbitMQ 服务节点 邮局
Exchange 接收消息并路由到队列 分拣中心
Queue 存储消息的缓冲区 邮箱
Binding Exchange 和 Queue 之间的绑定规则 分拣规则
Connection 客户端与 Broker 的 TCP 连接 运输通道
Channel Connection 中的轻量级虚拟连接 运输车

深度解析

一、消息流转全链路
  • 第一步 —— 生产者发送消息:Producer 将消息发送到 Broker 上的某个 Exchange,而不是直接发给 Queue。这是 RabbitMQ 和很多其他消息队列的一个重要区别。
  • 第二步 —— Exchange 路由:Exchange 收到消息后,根据自身的类型(direct、topic、fanout、headers)和 Binding 规则,决定将消息投递到哪些 Queue。如果没有任何 Queue 匹配,消息会被丢弃或返回给生产者(取决于配置)。
  • 第三步 —— 队列存储:消息进入 Queue 后,会持久化到磁盘(如果配置了持久化),等待消费。
  • 第四步 —— 消费者消费:Consumer 通过 Connection 和 Channel 连接到 Broker,从 Queue 中订阅或拉取消息进行处理。
  • 第五步 —— ACK 确认:消费者处理完消息后,发送 ACK 确认。如果消费者在处理过程中崩溃或未确认,消息会重新入队,确保不丢失。

关键点在于,Exchange 是整个路由的核心。理解了 Exchange 的四种类型,就理解了 RabbitMQ 的消息分发模型。

二、Exchange 四种类型对比

这是面试官最爱追问的点,务必搞清楚:

2. RabbitMQ 是如何保证高可用的?

面试考察点

  1. 集群方案掌握度:面试官不仅仅是想知道你知道有集群这回事,更是想确认你是否清楚普通集群、镜像队列、仲裁队列三者的区别和演进关系。
  2. 故障恢复机制:是否理解主从切换、脑裂处理、节点恢复等故障场景下的实际行为。
  3. 生产实践意识:能否结合实际场景说明高可用架构的部署方案(Federation、Shovel、负载均衡等),而不只是停留在概念层面。

核心答案

RabbitMQ 的高可用从 四个层面 来保证:

层面 机制 解决什么问题
集群层面 多节点组成集群,元数据共享 单点故障
队列层面 镜像队列 / 仲裁队列实现数据复制 数据丢失
网络层面 Federation / Shovel 实现跨集群通信 异地多活、跨机房
接入层面 HAProxy + Keepalived 负载均衡 + VIP 漂移 透明故障切换

深度解析

一、普通集群——只解决元数据共享

普通集群是最基础的部署方式。它的核心思路是:每个节点都有全量的元数据(Exchange、Binding、Queue 的声明信息),但队列的实际数据只存在于主节点上。

  • 元数据同步:每个节点都知道集群中有哪些 Queue、Exchange、Binding,消费者连接任何节点都能找到 Queue 所在的主节点
  • 数据不复制:消息本身只存在主节点,其他节点收到消费请求时会临时从主节点拉取数据(带来网络开销)
  • 致命缺陷:主节点一旦挂了,该节点上的 Queue 数据直接丢失,直到主节点恢复才能重新可用

所以普通集群**解决的是 “节点挂了不影响路由查找”,但不解决 “数据不丢”**。

二、镜像队列——主从复制(已弃用)

镜像队列就是在普通集群的基础上,把 Queue 的数据复制到其他节点上。

  • 主从模式:Queue 有一个 Master 和多个 Slave,写操作先到 Master,再同步到 Slave
  • 自动故障切换:Master 挂了,最老的 Slave 被提升为新的 Master
  • 同步策略:通过 ha-sync-mode 配置,automatic 为同步复制(安全但慢),manual 为异步复制(快但可能丢数据)

但这里有个大坑:镜像队列在 RabbitMQ 3.13 已被弃用,RabbitMQ 4.x 彻底移除。原因是什么呢?

  • 主从同步采用 Guaranteed Multicast 机制,实现复杂且不够可靠
  • 脑裂场景处理不够优雅
  • 性能开销大,特别是在节点数多的时候

面试时主动提到这个版本差异,面试官会觉得你关注技术演进,加分。

三、仲裁队列——当前推荐方案

仲裁队列(Quorum Queue)是 RabbitMQ 3.8 引入、官方大力推荐的新方案,基于 Raft 共识算法 实现。

3. RabbitMQ 如何防止重复消费?

面试考察点

  1. 问题溯源:面试官不仅仅是想知道怎么防重复,更是想确认你是否清楚重复消费是怎么产生的(网络抖动、消费者重启、ACK 超时等),而不是一上来就背方案。
  2. 幂等性设计:是否理解 “防重复消费” 的本质是业务幂等,能否给出具体的实现方案(唯一 ID、去重表、状态机等)。
  3. 方案选型能力:能否根据业务场景选择合适的幂等方案,而不是上来就 “用 Redis”。

核心答案

先理清一条关键逻辑:

先看重复消费产生的典型场景:根本原因在于:消费者处理完了业务逻辑,但在发送 ACK 之前挂了或网络断了,RabbitMQ 没收到 ACK,就会把消息重新投递。

所以核心思路是:不管消息被投递多少次,业务结果只生效一次——这就是幂等性。

常见的幂等方案对比:

方案 实现方式 适用场景 复杂度
唯一消息 ID + 去重表 给每条消息分配全局唯一 ID,消费前查去重表 通用方案,最常用 中
数据库唯一约束 利用表的主键或唯一索引防重复插入 新增类操作(下单、充值) 低
Redis SETNX 用 SETNX 命令判断是否已处理 高并发、对性能敏感 低
乐观锁 / 版本号 更新时带上版本号条件 更新类操作(扣库存、改余额) 中
状态机 业务状态只能单向流转(如 待支付 → 已支付) 订单、工单等有明确状态的业务 中

深度解析

一、唯一消息 ID + 去重表(最通用)

这是生产环境用得最多的方案,几乎所有场景都能覆盖。

原理:给每条消息分配一个全局唯一 ID(messageId),消费前先查去重表,如果已存在就跳过,不存在就处理并写入去重表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 消费者伪代码
@RabbitListener(queues = "order.queue")
public void handleOrder(Message message, Channel channel)
throws Exception {

String messageId = message.getMessageProperties().getMessageId();

try {
// 1. 查去重表:这条消息处理过没?
if (duplicateMapper.existsById(messageId)) {
// 处理过了,直接 ACK,别再投递了
channel.basicAck(message.getMessageProperties()
.getDeliveryTag(), false);
return;
}

// 2. 没处理过,执行业务逻辑
orderService.createOrder(parseOrder(message));

// 3. 写入去重表,标记已处理
duplicateMapper.insert(new DuplicateRecord(messageId));

// 4. 确认消费
channel.basicAck(message.getMessageProperties()
.getDeliveryTag(), false);

} catch (Exception e) {
// 业务异常,拒绝并重回队列
channel.basicNack(message.getMessageProperties()
.getDeliveryTag(), false, true);
}
}

十四、Kafka 面试题

共 8 题。

题目清单

  1. Kafka 的架构是怎么样的?
  2. Kafka 为什么这么快?
  3. Kafka 有几种选举过程?
  4. Kafka 如何实现顺序消费?
  5. Kafka 如何保证消息不丢失?
  6. Kafka 消息的发送过程简单介绍一下?
  7. 什么是 Kafka 的重平衡机制?
  8. 介绍一下 Kafka 的 ISR 机制?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. Kafka 的架构是怎么样的?

面试考察点

  1. 基础掌握度:别以为面试官只想听你背几个名词,他真正想看的是你能不能把 Broker、Topic、Partition、副本这些组件之间的关系讲清楚,串成一张完整的图。
  2. 架构演进认知:Kafka 这几年最大的变化就是去 ZooKeeper 化(KRaft 模式)。知不知道这个演进,是区分 “用过 Kafka” 和 “持续关注 Kafka” 的分水岭。
  3. 原理深度:分区副本机制、Leader/Follower、Controller 这些偏原理的东西,能不能在架构层面讲明白,决定了你这道题是拿 70 分还是 95 分。

核心答案

先说结论:Kafka 是一个分布式发布-订阅消息系统,整体架构可以概括为 “一横一纵一协调”:

  • 一横(存储层):Broker 集群 + Topic 分区 + 多副本,负责存储和高可用
  • 一纵(读写层):Producer 写入、Consumer Group 消费,负责数据流入流出
  • 一协调(管控层):老版本靠 ZooKeeper 管元数据,新版本(KRaft 模式)Kafka 自己管理
组件 一句话说明
Producer 生产者,往 Topic 里写消息,自带负载均衡
Broker 一个 Kafka 服务器实例,负责存储和转发消息
Topic 消息的逻辑分类,类似数据库里的表
Partition Topic 的物理分片,扩展性和顺序性的基本单位
Replica 分区副本,Leader 负责读写、Follower 负责同步
Consumer Group 消费者组,组内分摊分区,组间互不影响
ZooKeeper / KRaft 协调层,管元数据和 Controller 选举(新版已去 ZK 化)

深度解析

一、整体架构图

先上一张全局图,脑子里有这张图,后面聊什么都不慌:

Kafka 整体架构

对照这张图,把几个关键点捋一遍:

  • 写入只走 Leader:Producer 发消息,不管连的是哪个 Broker,消息最终都会路由到目标分区的 Leader 副本上,Follower 不接生产者的写请求
  • 消费也只走 Leader:Consumer 拉消息同样只从 Leader 拉,Follower 是纯备份
  • 分区打散在多个 Broker:Topic-A 的 Partition 0 和 Partition 1 分布在不同 Broker 上,写入压力就被分摊开了,这就是 Kafka 水平扩展的底气
  • 消费者组内分摊分区:一个分区同一时刻只会被组内一个消费者消费,Consumer 2 一个人扛 Topic-A 的 Partition 1 和 Topic-B 的 Partition 0,这是消费端负载均衡的基本盘
  • 协调层负责管 “谁是老大”:集群元数据、Controller 选举这些全局性决策,交给 ZooKeeper(老版本)或 KRaft(新版本)
二、核心组件逐个拆

2. Kafka 为什么这么快?

面试考察点

  1. 原理理解深度:面试官不想听你背名词,想知道的是你有没有真的理解顺序写、页缓存、零拷贝背后的操作系统原理,能不能讲清楚 “为什么这样快”。
  2. 操作系统功底:页缓存、sendfile、DMA、上下文切换,这些全是 OS 层面的知识。这道题答得好不好,直接暴露你计算机基础的扎实程度。
  3. 架构设计思维:批量、压缩、分区并行,这些是通用的性能优化思想。面试官想看你能不能把这些思想迁移到自己系统的设计里。
  4. 生产实战意识:知道这些机制后,会不会据此调优(比如 producer 的批量参数、分区数规划),这是 “会用” 和 “懂原理” 的分水岭。

核心答案

先甩结论,Kafka 的高吞吐是 6 大手段叠加 的结果:

优化手段 核心原理 解决的问题
顺序写磁盘 日志文件只追加,不随机写 磁盘随机 I/O 慢
Page Cache 读写都走 OS 页缓存,不进 JVM 堆 避免 GC、重启缓存不丢
零拷贝 sendfile() 数据不经过用户态 减少拷贝和上下文切换
批量发送 攒一批再发(batch.size + linger.ms) 摊薄网络往返开销
消息压缩 整批压缩(gzip/snappy/lz4/zstd) 省带宽、省存储
分区并行 Topic 拆成多个 Partition 水平扩展、并行读写

前三条是磁盘 I/O 和网络传输层面的 “降本”,后三条是架构层面的 “增效”。面试时按这个顺序讲,层次感就出来了。

深度解析

一、顺序写:磁盘其实不慢,慢的是随机写

很多人有个误区,觉得磁盘一定比内存慢。其实要分场景看。Kafka 官方设计文档给过一组数据(6 块 7200 转 SATA 盘组 RAID-5):顺序写约 600 MB/s,随机写约 100 KB/s——差了约 6000 倍。

Kafka 的做法简单粗暴:每个 Partition 在磁盘上就是一批日志文件(Log Segment),消息来了只做一件事——追加到文件末尾,写完就返回(默认不等刷盘)。顺序写对磁盘太友好了:磁头不用来回寻道,写入还天然有序,OS 也好批量刷盘。

Kafka 消息写入流程

  • 追加写:消息只往文件末尾追加,永远不修改已有数据,这是 Kafka 日志存储的根基
  • 写 Page Cache 就返回:Broker 收到消息写进页缓存就算成功,不用等真正落盘(持久化靠多副本保证,后面追问会讲)
  • OS 异步刷盘:操作系统攒一批后顺序刷下去,又是一个顺序写

顺便说一句,消费者消费消息也基本是顺序读(按 offset 从头到尾读),顺序读同样受益于磁盘预读(readahead)机制,一读就是一大块进缓存。

Kafka 顺序写原理

二、Page Cache:把缓存这事全权交给操作系统

Kafka 有个很反直觉的设计:它几乎不在 JVM 堆里缓存消息。数据读写的缓存,完全依赖操作系统的页缓存(Page Cache)。

3. Kafka 有几种选举过程?

面试考察点

  1. 体系化掌握:分区副本怎么选 Leader 只是基本盘,面试官更想看你脑子里有没有一张全局图——Kafka 里至少有三类选举(Controller、Partition Leader、Consumer Leader),答不全说明对架构的理解是碎片的。
  2. 原理深度:ZooKeeper 模式怎么选 Controller?KRaft 模式又怎么选?ISR 是什么?unclean 选举为什么可能丢数据?这些追问环环相扣,一问就见底。
  3. 生产意识:选举不是纸上谈兵——Broker 宕机后集群多久恢复、unclean.leader.election.enable 开了会怎样、脑裂怎么防,这些全是线上真踩过的坑。

核心答案

先说结论:Kafka 里主要有 三种选举:

选举类型 选出什么 谁来组织 核心机制
Controller 选举 集群的 “大管家” Broker ZK 模式:ZooKeeper;KRaft 模式:Raft 协议 先到先得 / 多数派投票
Partition Leader 选举 每个分区的主副本 Controller 从 ISR 中选,优先第一个副本
Consumer Leader 选举 消费组的 “组长” 消费者 GroupCoordinator 第一个加入组的消费者

下面一个一个拆开讲。

深度解析

一、Controller 选举:抢椅子游戏

Controller 是整个集群的大管家,负责分区 Leader 选举、故障转移、元数据管理这些活儿。一个集群只能有一个 Controller,那谁来当?这就涉及两种模式的演进了。

老模式:依赖 ZooKeeper(Kafka 2.8 之前)

所有 Broker 启动时都会去 ZooKeeper 的 /controller 节点上抢着注册——谁先创建成功,谁就是 Controller,输家们就监听这个节点。

Kafka Controller 选举流程

  • 先到先得:本质是抢分布式锁,谁的注册请求先落到 ZooKeeper,谁赢
  • 临时节点:Controller 所在 Broker 宕机后,ZooKeeper 会话失效,临时节点自动删除
  • 故障转移:其他 Broker 监听到节点被删,立刻重新发起竞争,新 Controller 上位后会把元数据缓存和 Leader 分配全部重算一遍

Kafka ZK 选举机制

新模式:KRaft(Kafka 2.8 引入,3.3 生产可用)

Kafka 后来把 ZooKeeper 扔了,改用内置的 KRaft 模式(KRaft = Kafka Raft)。Controller 选举不再依赖外部,而是用 Raft 协议自己选:


十五、JVM 面试题

共 26 题。

题目清单

  1. 有哪些常用的 JVM 启动参数?
  2. Java 是编译型还是解释型语言?
  3. Java 类加载的过程是怎样的?
  4. JVM 如何判断对象是否存活的?
  5. JVM 垃圾回收算法有哪些?
  6. 内存泄漏和内存溢出的区别是什么?
  7. 新生代和老年代的 GC 算法有哪些?
  8. 什么是强引用、软引用、弱引用和虚引用?
  9. 什么是双亲委派?怎么破坏?
  10. 项目中是如何选择垃圾回收器的?为啥选择这个?
  11. FullGC 多久一次正常?
  12. JVM 中一次完整的 GC 流程是怎样的?
  13. YoungGC 和 FullGC 的触发条件是什么?
  14. 什么情况会导致 JVM 退出?
  15. 什么是 Class 常量池,和运行时常量池区别是什么?
  16. 什么是 AOT 编译?和 JIT 有什么区别?
  17. JVM 运行时内存区域如何划分的?
  18. OOM 引起原因以及如何排查?
  19. CMS 和 G1 垃圾回收器的区别是什么?
  20. CMS 的底层原理是什么?优势在哪?
  21. 满足什么条件时,一个 Java 类会被卸载?
  22. JVM 对 Synchronized 关键字的实现是怎样的?
  23. Spring 第一次启动执行慢,从 JVM 角度讲讲?
  24. 线上 Full GC 频繁,如何排查解决?
  25. 运行时常量池和字符串常量池的关系是什么?
  26. JVM 类加载器如何保证核心类库不被覆盖?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 有哪些常用的 JVM 启动参数?

面试考察点

  1. 基础掌握度:你能不能把参数和 JVM 内存区域对应起来,而不是孤立地记住几个名字。
  2. 生产实践意识:有没有真实的调优经历,还是只看过书。
  3. 原理深度:不同 GC 的参数差异、G1 和 ZGC 的调优思路,能聊到这个层次说明功底扎实。

核心答案

常用参数按功能分 5 大类:

分类 代表参数 干嘛用的
内存设置 -Xms、-Xmx、-Xmn、-Xss 控制堆、栈大小
垃圾收集器 -XX:+UseG1GC、-XX:+UseZGC 选 GC 算法
GC 日志 -Xlog:gc*、-XX:+PrintGCDetails 排查 GC 问题
元空间 -XX:MetaspaceSize、-XX:MaxMetaspaceSize 控制方法区大小
故障诊断 -XX:+HeapDumpOnOutOfMemoryError OOM 时自动 dump

逐个说。

深度解析

一、内存设置参数

这部分是日常碰到最多的。

1
2
3
4
5
6
7
8
9
10
11
12
# 堆内存设置
-Xms512m # 初始堆大小(等同于 -XX:InitialHeapSize)
-Xmx2g # 最大堆大小(等同于 -XX:MaxHeapSize)
-Xmn256m # 年轻代大小
-Xss512k # 每个线程的栈大小(等同于 -XX:ThreadStackSize)

# 元空间设置(JDK 8+)
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=512m # 元空间最大大小

# 直接内存
-XX:MaxDirectMemorySize=256m

几个容易踩坑的地方:

  • -Xms 和 -Xmx 建议设成一样。堆动态伸缩会带来性能抖动,扩容要跟 OS 申请内存可能触发 GC,缩容还得整理。生产环境一般设物理内存的 60%~70%,留点给操作系统和元空间。
  • -Xmn 控制年轻代大小,但用 G1 的时候通常不设它,G1 靠 -XX:MaxGCPauseMillis 自己调节更靠谱。
  • -Xss 默认值看平台,Linux 64 位约 1MB,Windows 约 320KB。线程数特别多的场景(几万个)可以适当调小省内存。
  • 元空间用的是本地内存,不在堆里。-XX:MaxMetaspaceSize 不设的话默认没上限,我见过一台机器被元空间吃满的案例。生产环境一定加上限。
二、垃圾收集器参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Serial 收集器(单线程,Client 模式默认)
-XX:+UseSerialGC

# Parallel Scavenge + Parallel Old(JDK 8 默认,吞吐量优先)
-XX:+UseParallelGC
-XX:ParallelGCThreads=4 # GC 线程数

# CMS 收集器(JDK 9 废弃,JDK 14 移除,老项目可能还活着)
-XX:+UseConcMarkSweepGC

# G1 收集器(JDK 9+ 默认)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间(默认 200ms)
-XX:G1HeapRegionSize=8m # Region 大小(1/2/4/8/16/32MB)

# ZGC(JDK 15+ 生产可用)
-XX:+UseZGC
-XX:ZCollectionInterval=0 # GC 间隔,0 表示按需触发
-XX:SoftMaxHeapSize=1g # 软上限,尽量不超这个值

2. Java 是编译型还是解释型语言?

面试考察点

  1. 概念准确度:面试官不是让你做单选题。他想听你说清楚 Java 的执行机制,以及它跟 C++、Python 这些语言到底有什么不同。
  2. JVM 底层认知:字节码、JIT、AOT,这些概念你能不能串成一条线?很多人知道名词但连不起来。
  3. 横向对比能力:能不能跳出 Java 本身,从语言设计的角度去分析编译和解释的取舍。

核心答案

Java 两种都占了。

.java 源码先经过 javac 编译成 .class 字节码,这一步是编译。然后字节码交给 JVM,JVM 逐条解释执行,这一步是解释。所以它是 “先编译、后解释” 的混合模式。

执行方式 代表语言 怎么跑的
纯编译型 C、C++ 源码直接编译成机器码,跑得快但绑平台
纯解释型 Python、Ruby 源码逐行翻译执行,跨平台但慢
混合型 Java、Kotlin 编译成字节码,再由 JVM 解释或 JIT 编译执行

说个趣事,我之前面试一个候选人,他说完 “混合型” 之后补了一句:”Python 其实也有字节码编译,严格来说也不算纯解释型。” 就这一句话,直接加分。面试官喜欢能举一反三的人。

深度解析

一、从 .java 到机器码,中间发生了什么

整个过程拆开来看:

编译阶段,javac 把 .java 变成 .class。注意,这里产出的不是 x86 或 ARM 的机器指令,而是一套中间格式,跟具体 CPU 没关系。这就是 Java 跨平台的根基:字节码只有一套,不同平台装不同的 JVM 就行。当年 Sun 公司喊出 “Write Once, Run Anywhere”,底气就在这儿。

运行阶段,JVM 拿到字节码后怎么执行?两条路:

  • 解释器逐条翻译,边翻边跑。好处是启动快,坏处是每次都要翻译,同样的代码跑十次就翻译十次。
  • JIT 编译器不一样。它发现某段代码跑得特别频繁(热点代码),就一次性编译成机器码缓存起来,后面直接跑机器码,不再翻译了。
二、JIT 编译,Java 性能翻盘的关键

总有人说 “Java 慢”,这观念大概还停在 JDK 1.0 年代。那时候确实只有解释器,跑起来跟 Python 差不多。但从 HotSpot JVM 开始(JDK 1.2),JIT 编译器把这事给扳回来了。

HotSpot 里有两个 JIT 编译器,分工不同:

编译器 启动速度 峰值性能 谁在用
C1(Client) 快 中等 桌面应用
C2(Server) 慢 很高 服务端

JDK 7 开始搞了个分层编译:先用 C1 快速编译,保证启动速度;跑一会儿之后,把热点代码交给 C2 做深度优化。两层配合下来,长时间跑的服务端应用,性能跟 C/C++ 差距很小。JDK 8 之后分层编译默认开启,不用手动配。

3. Java 类加载的过程是怎样的?

面试考察点

  1. 流程完整性:类加载到底分几步?每一步干了什么?能不能把加载 → 链接 → 初始化这条线从头讲到尾,而不是背个名字就完事。
  2. 细节掌握度:链接阶段还细分为验证、准备、解析三个子步骤,这里面的区别你能说清楚吗?static 变量什么时候分配内存?什么时候赋初始值?什么时候赋真正值?这三个时间点搞混的人特别多。
  3. 实战关联:类加载机制跟双亲委派模型、自定义类加载器、SPI 机制这些都有关系,面试官很容易从这里追问下去。

核心答案

类加载分 5 个阶段:加载 → 验证 → 准备 → 解析 → 初始化。其中验证、准备、解析三步合起来叫 “链接”(Linking)。

阶段 干了什么 一句话概括
加载(Loading) 把 .class 文件的二进制数据读进内存 找到文件,读进来
验证(Verification) 检查字节码格式是否合法、有没有安全问题 安检
准备(Preparation) 为 static 变量分配内存,赋零值 分座位,先坐好
解析(Resolution) 把常量池里的符号引用替换成直接引用 把名字翻译成地址
初始化(Initialization) 执行 <clinit>() 方法,赋真正值 正式开工

还有个容易被忽略的第六步:卸载(Unloading),类被 GC 回收。不过这块面试基本不问,知道有这回事就行。

深度解析

一、全流程图

整个流程是顺序的:加载 → 链接(验证 → 准备 → 解析)→ 初始化。不过 “解析” 这一步在某些情况下可以在初始化之后才开始(支持 Java 的运行时绑定,也就是多态)。

下面逐个拆开讲。

二、加载(Loading)

这一步做三件事:

  • 通过类的全限定名找到对应的 .class 文件
  • 把二进制数据读进内存,存到方法区(JDK 8 以后叫 Metaspace)
  • 在堆里生成一个对应的 java.lang.Class 对象,作为方法区数据的访问入口

注意,”加载” 这个词在日常聊天中经常泛指整个类加载过程,但在 JVM 规范里它只是第一步。面试时别搞混。

.class 文件不一定来自磁盘,还可以从 JAR/WAR 包读取、通过网络下载、甚至运行时动态生成(动态代理就是这么干的)。

三、验证(Verification)

这一步是安全检查。JVM 怕字节码被人篡改或者编译器有 bug,所以要验一遍。验证的内容包括:

  • 文件格式:是不是以魔数 0xCAFEBABE 开头?版本号对不对?
  • 元数据:有没有继承一个 final 类?有没有实现不存在的接口?
  • 字节码:指令跳转有没有越界?类型转换合不合法?
  • 符号引用:引用的类、方法、字段存不存在?

其中格式验证在加载阶段就会开始,不是非要等到链接。

4. JVM 如何判断对象是否存活的?

面试考察点

  1. 算法认知:你知道哪些判断对象存活的方法?能不能说清楚引用计数法和可达性分析各自的原理和优缺点?
  2. GC Roots 的理解:哪些东西能作为 GC Roots?这个背不下来很正常,但至少要能说出最常见的几种。
  3. 引用类型的区分:强、软、弱、虚四种引用,各自什么时候被回收?跟实际开发有什么关系?很多候选人知道 WeakReference 但说不清什么时候用。

核心答案

JVM 用的是可达性分析算法(Reachability Analysis)。从一组叫 “GC Roots” 的根对象出发,顺着引用链往下找。能找到的对象就是活的,找不到的就是死的,可以被回收。

不过在讲可达性分析之前,得先提一个已经被淘汰的方案——引用计数法。面试官经常拿它当铺垫来问。

深度解析

一、引用计数法(已被淘汰)

思路很简单:给每个对象维护一个计数器,被引用一次 +1,引用断开 -1。计数器为 0 就说明没人用了,可以回收。

引用计数法的致命缺陷就是循环引用。两个对象互相指着对方,计数器永远不为 0,但实际上外面已经没人用它们了。这块内存就泄露了。

Python 用的是引用计数法,但它额外配了一套分代 GC 来解决循环引用的问题。而 Java 从一开始就没选这条路,直接用了可达性分析。

二、可达性分析算法(JVM 实际使用的方案)

从 “GC Roots” 出发,沿着引用链往下搜。搜索走过的路径叫 “引用链”(Reference Chain)。如果一个对象到 GC Roots 之间没有任何引用链可达,说明这个对象不可达,可以被回收。

上面这幅图很直观。GC Roots 是起点,能连上的就是活的,连不上的就是死的。

但你可能会问:到底哪些对象能当 GC Roots?

三、哪些对象可以作为 GC Roots

这个问题面试官几乎必问。能当 GC Roots 的有四类:

  1. 虚拟机栈中引用的对象:各个线程方法里的局部变量。方法正在执行时,里面的局部变量指向的对象肯定不能回收。
  2. 方法区中类静态变量引用的对象:static 变量引用的对象。类的生命周期没结束,它引用的对象就得留着。
  3. 方法区中常量引用的对象:比如 static final 修饰的常量引用的对象。
  4. 本地方法栈中 JNI 引用的对象:native 方法里引用的对象。

还有几个在特定场景下也会被视为 GC Roots:

  • 同步锁(synchronized)持有的对象
  • JVM 内部的引用:基本数据类型的 Class 对象、常驻的异常对象(NullPointerException 等)、类加载器
  • JMXBean、JVMTI 中注册的回调、本地代码缓存

面试时把前四个说清楚就够了。后面的属于加分项,说出来面试官会觉得你研究过源码。

四、四次生死判定——真正被回收要过几关

一个对象被判定为不可达,不等于立刻就被回收。还要过两道坎:

5. JVM 垃圾回收算法有哪些?

面试考察点

  1. 算法基础掌握:面试官想知道你是否清楚四种基础 GC 算法的原理和特点,而不是笼统地说一句 “标记清除”。
  2. 优缺点对比能力:每个算法都有明显的短板,面试官考察你能否说出 “空间碎片”、”复制开销”、”Stop-The-World” 这些关键词,以及它们各自的适用场景。
  3. 分代收集思想:现代 JVM 不是只用一种算法,而是根据分代特点组合使用。能不能把这层关系讲清楚,是区分 “背八股文” 和 “真理解” 的分水岭。

核心答案

JVM 的垃圾回收算法有 4 种基础算法:

算法 核心思路 优点 缺点
标记-清除(Mark-Sweep) 先标记可回收对象,再统一清除 实现简单 空间碎片多、分配效率低
标记-复制(Copying) 将存活对象复制到另一块空间,原空间整体清空 没有碎片、分配快(指针碰撞) 内存利用率只有一半
标记-整理(Mark-Compact) 将存活对象向一端移动,清理边界外空间 没有碎片、内存利用率高 移动对象开销大
分代收集(Generational) 根据对象存活特点分代,每代选用最合适的算法 兼顾效率和利用率 实现复杂

现代 JVM(HotSpot)采用的是 分代收集策略,在不同代中组合使用上述基础算法。

深度解析

一、标记-清除算法(Mark-Sweep)

最基础的 GC 算法,分为两步:先标记出所有需要回收的对象,再统一清除。

标记-清除的过程很直观:遍历所有对象,把不可达的标出来(标记阶段),然后把这些对象的内存直接回收(清除阶段)。

  • 优点:实现简单,不需要移动对象,不需要额外空间。
  • 缺点一:空间碎片。清除后内存中布满了大大小小的空闲块。假设现在要分配一个 3 个单位的对象,虽然总空闲空间够,但没有任何一个连续的空闲块满足要求,分配就会失败。
  • 缺点二:分配效率低。碎片化之后,分配器不得不用 “空闲列表”(Free List)来管理可用内存,每次分配都要遍历列表找合适大小的块,比起 “指针碰撞”(Bump the Pointer)那种 O(1) 的分配方式,效率差了不少。
二、标记-复制算法(Copying)

为了解决碎片问题,标记-复制算法把内存一分为二,每次只用其中一半。

标记-复制的思路很巧妙:每次 GC 只扫描当前正在使用的半区(From 区),把存活对象 按顺序 复制到另一半(To 区),然后 From 区整个清空。复制完成后两个角色互换。

  • 优点:复制过去之后对象是紧凑排列的,没有碎片。分配新对象时只需要移动一个指针就行了(指针碰撞),O(1) 搞定,效率极高。
  • 缺点:太费内存了。可用空间直接打了个五折,对于堆内存寸土寸金的 JVM 来说,这个代价很大。另外如果存活对象很多,复制操作本身的开销也不小。

十六、计算机网络面试题

共 14 题。

题目清单

  1. 什么是 TCP 三次握手、四次挥手?
  2. Cookie、Session、Token 的区别是什么?
  3. TCP 是怎么保证可靠传输的?
  4. HTTPS 和 HTTP 的区别是什么?
  5. 对称加密和非对称加密有什么区别?
  6. TCP 和 UDP 的区别是什么?
  7. 什么是跨域访问问题,怎么解决?
  8. 什么是 TCP 的粘包、拆包问题?
  9. 什么是网络分区?
  10. 什么是正向代理和反向代理?
  11. 什么是 IPv6?和 IPv4 有什么区别?
  12. 什么是 CDN?为什么它可以做缓存?
  13. 路由器与交换机的区别是什么?
  14. 浏览器输入 www.quanxiaoha.com,回车之后发生了什么?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 什么是 TCP 三次握手、四次挥手?

面试考察点

  1. 网络基础掌握度:面试官不仅仅是想知道你能否背诵三次握手、四次挥手的流程,更是想确认你是否理解 TCP 作为可靠传输协议的核心设计思想,以及为什么需要这样设计。
  2. 原理理解深度:考察你是否清楚每次握手/挥手的目的、状态变化、以及 “为什么是三次而不是两次”、”为什么挥手需要四次” 等深层原因。
  3. 实践关联能力:能否将理论知识与实际场景联系起来,比如连接建立失败如何排查、TIME_WAIT 状态过多怎么办等生产问题。

核心答案

TCP 三次握手是建立可靠连接的过程,客户端和服务端共交互 3 次,确保双方都具有发送和接收数据的能力。

TCP 四次挥手是断开连接的过程,需要 4 次 交互,确保双方都能优雅地关闭连接,避免数据丢失。

对比项 三次握手 四次挥手
目的 建立可靠连接 优雅断开连接
交互次数 3 次 4 次
核心标志位 SYN、ACK FIN、ACK
谁发起 客户端 客户端或服务端
最终状态 ESTABLISHED CLOSED

深度解析

一、TCP 三次握手详解
  1. 第一次握手(Client → Server):

    • 客户端发送 SYN=1 报文,携带初始序列号 seq=x
    • 客户端进入 SYN_SENT 状态,等待服务端确认
    • 这个阶段客户端告诉服务端:”我想和你建立连接”
  2. 第二次握手(Server → Client):

    • 服务端收到 SYN 报文后,发送 SYN=1, ACK=1 报文
    • 携带服务端的初始序列号 seq=y,以及确认号 ack=x+1(确认收到客户端的 seq)
    • 服务端进入 SYN_RCVD 状态
    • 这个阶段服务端告诉客户端:”收到了你的请求,我也想和你建立连接”
  3. 第三次握手(Client → Server):

    • 客户端收到服务端的确认后,发送 ACK=1 报文
    • 携带 seq=x+1 和 ack=y+1(确认收到服务端的 seq)
    • 双方都进入 ESTABLISHED 状态,连接建立成功
    • 这个阶段客户端告诉服务端:”收到你的确认,我们可以开始通信了”
为什么必须是三次握手?

核心原因:防止已失效的连接请求报文突然又传送到了服务端,造成资源浪费。

假设采用两次握手:

三次握手的本质目的:

  • 确认双方的接收能力和发送能力都正常
  • 同步双方的初始序列号(ISN)
  • 防止历史连接造成资源浪费
二、TCP 四次挥手详解
  1. 第一次挥手(Client → Server):

    • 客户端发送 FIN=1 报文,表示没有数据要发送了
    • 客户端进入 FIN_WAIT_1 状态
    • 本质:客户端告诉服务端 “我说完了,准备关闭”
  2. 第二次挥手(Server → Client):

2. Cookie、Session、Token 的区别是什么?

面试考察点

  1. 基础概念掌握度:面试官不仅仅是想知道你能说出三者的定义,更是想确认你是否理解它们在 Web 认证和状态管理中的角色定位,以及各自的存储位置和工作机制。
  2. 架构演进意识:考察你是否清楚从 Cookie + Session 到 Token 的演进过程,理解为什么现代分布式系统更倾向于使用 Token,以及这背后的技术驱动因素。
  3. 实践应用能力:能否根据实际场景选择合适的方案,比如单机应用用 Session、分布式系统用 Token、移动端如何处理认证等,以及了解各自的安全风险和防范措施。

核心答案

Cookie 是浏览器存储的小型数据,由服务器生成、浏览器保存;Session 是服务器端存储的会话数据,通过 Cookie 中的 SessionID 关联;Token 是无状态的身份凭证,包含用户信息且可自验证。

对比维度 Cookie Session Token(JWT)
存储位置 浏览器(客户端) 服务器内存/Redis 客户端(localStorage/Cookie)
安全性 较低(容易被窃取) 较高(数据在服务端) 较高(签名防篡改)
跨域支持 受同源策略限制 需 SessionID 跨域 天然支持跨域
服务器压力 无 有(需存储 Session) 无(无状态)
扩展性 一般 差(需 Session 共享) 好(天然支持分布式)
移动端适配 不友好 不友好 友好
典型场景 存储偏好设置 传统单体应用认证 分布式/移动端认证

一句话总结:Cookie 是容器,Session 是服务端状态,Token 是自包含凭证;现代分布式架构首选 Token。

深度解析

一、Cookie 详解
  1. 首次请求:浏览器向服务器发起请求,此时没有任何 Cookie 信息。
  2. 服务器设置 Cookie:服务器通过响应头 Set-Cookie 返回 Cookie 数据,格式为 name=value,还可设置过期时间、域名、路径、安全属性等。
  3. 浏览器存储:浏览器收到响应后,将 Cookie 保存在本地(内存或硬盘,取决于是否设置过期时间)。
  4. 自动携带:之后每次向同一域名发请求时,浏览器会自动在请求头中携带 Cookie 字段,服务器据此识别用户。

3. TCP 是怎么保证可靠传输的?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你能否背诵 TCP 的特性,更是想知道你是否理解可靠传输的底层机制——从校验和到流量控制,从确认应答到重传机制,每一层是如何协同工作的。
  2. 系统性思维:考察你是否能从发送端、接收端、网络环境三个维度来系统分析可靠性保障,而不是零散地列举知识点。
  3. 实践应用意识:如果你了解这些机制,在实际开发中遇到网络问题时(如超时、丢包),能否快速定位是哪一层的可靠性机制出了问题。

核心答案

TCP 通过 7 大核心机制 协同保障可靠传输:

机制 作用 解决的问题
校验和 检测数据在传输过程中是否变化 数据损坏
序列号 给每个字节编号,确保有序 乱序、重复
确认应答 (ACK) 告知发送方已收到哪些数据 丢包检测
重传机制 超时或收到重复 ACK 时重发 数据丢失
流量控制 滑动窗口控制发送速率 接收方处理不过来
拥塞控制 动态调整发送窗口 网络拥堵
连接管理 三次握手、四次挥手 建立和终止可靠连接

一句话总结:TCP 通过 “编号-确认-重传” 三部曲,配合流量控制和拥塞控制,在不可靠的 IP 层之上构建了可靠的数据传输通道。

深度解析

一、序列号与确认应答 (ACK) —— 可靠传输的基石

TCP 给每个字节的数据都分配了一个序列号(Sequence Number,简称 SEQ),接收方通过确认应答(Acknowledgment Number,简称 ACK)告诉发送方:”我期望收到的下一个序列号是多少”。

  1. **序列号 (SEQ)**:发送方发送数据时,TCP 头部携带序列号,表示这段数据的第一个字节的编号。例如 SEQ=1 表示这 100 字节的编号是 1~100。
  2. **确认号 (ACK)**:接收方回复的 ACK 号表示”我期望收到的下一个字节的序列号”。收到 SEQ=1 的 100 字节后,回复 ACK=101,表示 “1~100 我都收到了,请从 101 开始发”。
  3. 累积确认:TCP 采用累积确认机制,ACK=201 表示 200 及之前的所有数据都已正确接收。即使中间有乱序到达的包,也必须等到中间缺失的包补齐后才能确认。
二、重传机制 —— 应对丢包的利器

网络中丢包是常态,TCP 通过两种重传机制来保证数据最终送达:

1. 超时重传 (RTO)

发送方发出数据后启动一个定时器(RTO,Retransmission Timeout),如果在定时器到期前没有收到 ACK,就重传数据。

  1. RTO 计算:RTO 不是固定值,而是根据网络往返时间(RTT)动态计算的。经典公式:RTO = RTT均值 + 4 × RTT偏差,这样既能快速响应,又能避免误判。
  2. 指数退避:如果第一次重传仍然超时,RTO 会指数级增长(×2),避免在网络拥塞时雪上加霜。
  3. 超时阈值:Linux 默认重传次数为 15 次,超过后放弃连接。

4. HTTPS 和 HTTP 的区别是什么?

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道 HTTPS 比 HTTP “更安全”,更是想知道你是否理解安全的本质——数据是如何被加密的、证书的作用是什么、整个安全通信链路是如何建立的。
  2. 原理理解深度:考察你是否了解 HTTPS 的底层实现,包括 SSL/TLS 握手过程、对称加密与非对称加密的结合使用、数字证书的验证链等核心机制。
  3. 实践应用意识:如果你了解 HTTPS 的性能开销、证书部署方式、以及在实际项目中如何正确配置 HTTPS,说明你不只是 “懂理论”,还有 “实战经验”。

核心答案

HTTP 和 HTTPS 的核心区别如下:

对比维度 HTTP HTTPS
全称 HyperText Transfer Protocol HyperText Transfer Protocol Secure
端口 80 443
安全性 明文传输,数据裸奔 SSL/TLS 加密,数据安全
证书 不需要 需要 CA 颁发的数字证书
性能 无加密开销,更快 握手有开销,但现代优化后差距很小
SEO 无加成 搜索引擎优先收录

一句话总结:HTTPS = HTTP + SSL/TLS,通过加密传输和身份认证,解决了 HTTP 的三大安全问题——窃听、篡改、冒充。

深度解析

一、HTTP 的三大安全缺陷

在了解 HTTPS 之前,先要搞清楚 HTTP 为什么 “不安全”:

  1. 窃听风险:HTTP 数据以明文形式传输,就像寄信时把信封敞开,任何中间节点(路由器、代理、黑客)都能看到内容。你在咖啡厅连 WiFi 登录网银,旁边的黑客就能抓包看到你的账号密码。
  2. 篡改风险:HTTP 没有完整性校验机制,数据在传输过程中被修改,接收方根本不知道。运营商可以在网页里插入广告,黑客可以修改下载的软件包植入木马。
  3. 冒充风险:HTTP 没有身份验证机制,客户端无法确认服务器的真实身份。你以为是 “工商银行”,其实是黑客搭建的钓鱼网站。
二、HTTPS 如何解决这三大问题

HTTPS 通过 SSL/TLS 协议 层层设防,彻底解决了上述三个问题:

  1. 加密解决窃听:使用对称加密算法(如 AES)加密数据,即使黑客截获数据包,看到的也是一堆乱码,没有密钥就无法解密。
  2. 校验解决篡改:使用消息认证码(HMAC)对数据进行签名,接收方验证签名,如果数据被篡改,签名就对不上,直接丢弃。
  3. 证书解决冒充:服务器必须持有 CA 机构颁发的数字证书,客户端会验证证书的合法性,确保你访问的是真正的服务器,而不是钓鱼网站。
三、HTTPS 的核心:SSL/TLS 握手过程

十七、操作系统面试题

共 21 题。

题目清单

  1. GPU 和 CPU 的区别?为什么挖矿、大模型都用 GPU?
  2. 什么是操作系统的多级缓存?
  3. 什么是用户态、内核态?如何切换的?
  4. 同步、异步、阻塞、非阻塞怎么理解?
  5. 操作系统的 IO 模型有哪些?
  6. 什么是 CPU 利用率?怎么算的?
  7. 什么是时间片?
  8. 什么是 Load(负载)?
  9. 负载(Load)和 CPU 利用率之间有什么区别?
  10. 什么是 IO 密集,什么是 CPU 密集?
  11. 进程、线程和协程的区别是什么?
  12. 进程间通信方式有哪些?
  13. 什么是 PageCache?它的读写过程是怎么样的?有什么优缺点?
  14. 什么是全双工和半双工?
  15. 什么是零拷贝?
  16. 什么是分段和分页?
  17. 为什么按位与运算要比取模运算高效?
  18. IO 多路复用和多线程有什么区别?
  19. 如何理解 select、poll、epoll?
  20. 什么是 MESI 缓存一致性协议?
  21. 你掌握哪些 Linux 常用命令?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. GPU 和 CPU 的区别?为什么挖矿、大模型都用 GPU?

面试考察点

  1. 架构理解深度:面试官不仅仅是想知道 “GPU 核多 CPU 核少”,更是想知道你懂不懂两者在芯片设计哲学上的根本差异——为什么 CPU 不能堆成千上万个核?为什么 GPU 一个核这么弱?背后是 “延迟” 和 “吞吐” 两种设计取向的取舍。
  2. 并行计算认知:考察你是否理解什么样的任务适合 GPU,什么样的任务必须用 CPU。这直接决定你能不能在大模型训练、推理、数据并行这些场景里做出正确的技术选型。
  3. 场景迁移能力:挖矿和大模型看似是两个领域,但底层共性是 “海量重复计算”。面试官想看的是你能不能把这个共性抽象出来,而不是分别背两套答案。

核心答案

一句话总结:CPU 是 “几个老教授”,GPU 是 “几万个小学生”。 老教授能解微积分,但一次只能解一道题;小学生只会加减乘除,但一万个人同时算,总吞吐量吊打老教授。

对比维度 CPU GPU
核心数量 几个 ~ 几十个(消费级最多 32 核左右) 几千 ~ 几万个(如 RTX 4090 有 16384 个 CUDA Core)
单核能力 极强(复杂逻辑、分支预测、乱序执行) 弱(简单运算,注重吞吐)
晶体管分配 约 30% ALU,70% 控制逻辑 + 缓存 约 90% ALU,10% 控制和缓存
设计目标 低延迟(尽快完成单个任务) 高吞吐(同时处理海量任务)
内存带宽 DDR5 ~ 100 GB/s 级别 HBM3 ~ 3 TB/s 级别(差一个数量级)
擅长场景 操作系统调度、复杂业务逻辑、数据库、分支密集型 矩阵运算、图形渲染、哈希计算、神经网络训练

挖矿和大模型都是 “大规模、可并行、计算密集” 的任务,正好踩在 GPU 的设计甜区上。

CPU GPU 并行计算对比

深度解析

一、架构对比:晶体管怎么分配的

先把芯片的内部结构画出来,看一眼就懂了。

CPU GPU 架构对比

  • CPU 的核心设计:单核里塞了大量控制单元——分支预测器、乱序执行引擎、深层流水线(15~20 级),还有动辄几十 MB 的多级缓存。这些都是为了让 “单个任务跑得尽可能快”。代价是占用了大量晶体管,留给算术单元(ALU)的不到 30%。
  • GPU 的核心设计:走的是另一个极端——把控制逻辑压到最简,缓存很小,把 90% 的晶体管都堆给了 ALU。一个 SM(Streaming Multiprocessor,流多处理器)里能塞 128 个甚至更多的 ALU,整张卡几千个 ALU 同时干活。单个 ALU 弱得一批,但架不住人多。
  • 本质差异:CPU 追求 “把一件事做得飞快”(低延迟),GPU 追求 “同时做千万件事”(高吞吐)。这俩其实是互补的——CPU 当 “指挥官” 负责调度,GPU 当 “军队” 负责冲锋。
二、为什么挖矿要用 GPU

先说挖矿的本质。比特币挖矿(SHA-256 哈希)和早期以太坊挖矿(Ethash)的核心逻辑是:

2. 什么是操作系统的多级缓存?

面试考察点

  1. 硬件层级理解:面试官不仅仅是想知道你能不能背出 L1/L2/L3,更是想知道你是否清楚从 CPU 到磁盘之间存在一个完整的存储层次结构,以及每一级的速度和容量量级。
  2. 原理认知:是否理解多级缓存背后的两个核心原理——局部性原理(时间局部性 + 空间局部性),以及 CPU 与内存之间的“存储墙”问题。
  3. 与编程的联系:能不能把这个看似底层的硬件知识和实际开发联系起来,比如缓存行、伪共享、volatile 可见性、Disruptor 框架。这才是中大厂面试官真正想听的。

核心答案

操作系统的多级缓存,说白了就是计算机体系结构为了填平 CPU 和内存之间那道 100 倍以上的速度鸿沟,在两者之间插进去的一组存储层——越靠近 CPU 越快越小越贵,越往外越慢越大越便宜。

从 CPU 向外,整体是个阶梯结构:

层级 名称 访问延迟 典型容量 位置
L0 寄存器(Register) < 1 ns 几百 B CPU 核心内
L1 L1 高速缓存 ~1 ns 32~64 KB / 核 CPU 核心内
L2 L2 高速缓存 310 ns 256 KB~1 MB / 核 CPU 核心内
L3 L3 高速缓存 1020 ns 几 MB~几十 MB CPU 内(多核共享)
L4 主内存(DRAM) ~100 ns GB 级 主板
L5 磁盘缓存(Page Cache) μs 级 GB 级 操作系统内存区
L6 磁盘(SSD/HDD) 10 μs~10 ms TB 级 外部存储

这里要稍微澄清一下:L1/L2/L3 属于 CPU 硬件缓存,Page Cache 才是操作系统管理的磁盘缓存。面试时说“操作系统的多级缓存”其实是个宽泛说法,指的是整个存储层次。

多级缓存层级结构

深度解析

一、为什么要搞这么多级?核心是存储墙

这里有个矛盾必须先讲清楚:

  • 现代 CPU 时钟频率动辄 3 GHz+,执行一条指令不到 1 ns
  • 主内存(DRAM)一次访问要 ~100 ns
  • 中间差了 100 多倍

如果 CPU 每条指令都直接去内存取数据,那它 99% 的时间都在干等,性能崩盘。

好在程序运行有两个天然规律,叫局部性原理:

  • 时间局部性:刚被访问过的数据,短期内大概率还会被访问(比如循环里的变量)
  • 空间局部性:被访问数据附近的那些数据,也很可能马上被访问(比如数组、顺序指令)

基于这两个原理,硬件设计师就在 CPU 和内存之间塞了一组缓存,把热数据放在又快又贵、但容量做不大的存储里。

二、CPU 多级缓存结构

CPU 缓存结构

3. 什么是用户态、内核态?如何切换的?

面试考察点

  1. 基础概念掌握:面试官不只是想知道你知不知道这两个名词,更想看你是否理解为什么要区分这两种状态——考察的其实是操作系统的保护与隔离思想。
  2. 机制原理深度:能否说清楚用户态和内核态之间是怎么来回切的(系统调用、中断、异常),以及切换过程中 CPU、寄存器、栈发生了什么。
  3. 性能与工程意识:知不知道这种切换是有代价的,以及实际开发中如何减少不必要的切换(比如 epoll 为什么比 select 高效、BufferedReader 为什么能减少系统调用)。

核心答案

直接给结论:

  • 用户态(User Mode):进程运行在自己私有的地址空间里,只能访问 “用户空间” 的内存,不能直接碰硬件,也不能访问内核的代码和数据。CPU 处于低特权级(x86 上一般是 Ring 3)。
  • 内核态(Kernel Mode):操作系统内核运行的特权模式,可以访问所有内存、所有硬件资源,执行任何 CPU 指令。CPU 处于高特权级(x86 上是 Ring 0)。
  • 切换方式:从用户态 → 内核态有 三种触发方式——系统调用(主动)、异常(被动)、外部硬件中断(被动)。从内核态 → 用户态则是执行完内核逻辑后通过特殊的返回指令切回去。

简单一张表:

对比项 用户态 内核态
特权级(x86) Ring 3 Ring 0
可访问内存 只能访问本进程用户空间 全部内存空间
可执行指令 普通指令,不能执行特权指令 所有指令(含特权指令,如 I/O、关中断)
是否可直接访问硬件 否 是
运行的代码 应用程序 操作系统内核

为什么要这么设计?一句话:保护系统不被搞坏。如果用户程序能随便访问硬件、改内核数据,一个写得烂的程序(或者恶意程序)就能让整个系统崩溃。这种 “特权隔离” 的思想,是现代操作系统的根本。

用户态内核态隔离

深度解析

一、用一张图理解切换的全过程

用户态内核态切换

用户态内核态切换流程

  • 触发阶段:应用程序在用户态跑得好好的,遇到需要内核帮忙的事情(比如读文件、网络 I/O),就主动发起 系统调用;或者出了意外(除零、缺页),触发 异常;再或者外部硬件(时钟、网卡)打断 CPU,触发 中断。
  • 切换阶段:CPU 从 Ring 3 切到 Ring 0,把当前的寄存器、程序计数器(PC)、栈指针都保存起来(一般是压到内核栈),然后切到内核栈继续执行。
  • 执行阶段:内核根据系统调用号找到对应的内核函数(比如 sys_read),执行真正的逻辑。
  • 返回阶段:内核执行完后,恢复之前保存的用户态上下文,通过 iret 或 sysret 这类特权指令,把 CPU 特权级切回 Ring 3,应用程序接着跑。
二、三种切换触发方式

很多人答这道题只会说 “系统调用”,其实完整答案是三种。

1. 系统调用(主动切换)

4. 同步、异步、阻塞、非阻塞怎么理解?

面试考察点

  1. 概念区分度:面试官不仅仅是想知道你能不能背出四个定义,更是想知道你是否意识到这是两组正交的概念:一组描述消息通知机制,一组描述等待时的线程状态。
  2. IO 模型的映射能力:能不能把这四个概念落到具体的 IO 模型上(BIO、NIO、AIO、多路复用),而不是停留在抽象名词层面。
  3. 技术视野:是否了解 Linux 下 epoll、io_uring、Windows IOCP 这些真实实现,知道 “异步” 在操作系统层面到底是怎么兑现的。

核心答案

先甩结论:同步/异步说的是 “结果怎么拿”,阻塞/非阻塞说的是 “等结果的时候你能不能走”。这是两个不同维度,不是反义词的两端。

维度 关注点 一句话区分
同步 vs 异步 被调用方如何通知结果 同步:结果出来前,调用方得自己盯着;异步:被调用方办完了主动喊你
阻塞 vs 非阻塞 调用方等待时的状态 阻塞:调用发起后线程挂起干等;非阻塞:调用立即返回,线程该干嘛干嘛

两者组合,就得到了四种经典 IO 形态:

组合 名称 典型代表
同步阻塞 BIO Java Socket、老式 read()
同步非阻塞 NIO + 轮询 recv(MSG_DONTWAIT)、Java NIO
同步非阻塞 + 多路复用 IO 多路复用 select / poll / epoll
异步非阻塞 AIO Windows IOCP、Linux io_uring

注意,没有 “异步阻塞” 这个组合——都异步通知了还阻塞等着,逻辑上矛盾,属于人为造出来的伪命题。

同步异步阻塞非阻塞

深度解析

一、先上烧水的比喻,绕不过去的经典

小明想烧壶开水。这四个概念用烧水场景翻译一遍:

  • 同步阻塞:小明把水壶放灶上,然后站在灶边盯着,水不开哪儿也不去。人(线程)被这件事完全占住了。
  • 同步非阻塞:小明把水壶放灶上,转身去看电视了,每隔几分钟跑回厨房瞅一眼 “开了没”。人没被占住,但 “水开了” 这个结果还得小明自己来确认。
  • 同步非阻塞 + 多路复用:小明烧的不止一壶水,厨房里十几个灶眼。他雇了个管家(epoll),谁家水开了管家就喊一声,小明再过去处理。一个 “人” 搞定了 N 个灶。
  • 异步非阻塞:小明用的是会响的水壶,水开了 “嘀嘀” 自动叫,而且壶自己把水灌进暖瓶,小明只需要处理 “灌好的开水”。连 “确认水开” 和 “灌水” 这步都省了。

看出差别了吗?同步和异步的差别在于 “谁去确认结果”,阻塞和非阻塞的差别在于 “等的时候人在不在场”。

IO 模型烧水比喻

二、回到操作系统层面

抽象比喻落到代码上,以 read() 读网卡数据为例。数据从网卡到你的程序,要经历两个阶段:

read IO 两阶段

  • 阶段一:数据从网卡到达内核缓冲区(等待数据就绪)
  • 阶段二:数据从内核缓冲区拷贝到用户空间(真正的 IO 操作)

5. 操作系统的 IO 模型有哪些?

面试考察点

  1. 基础掌握度:面试官可不是想听你背 5 种模型的名字,他想搞清楚你明不明白每种模型的工作流程、阻塞点在哪里、数据是怎么从内核走到用户空间的。
  2. 底层理解深度:考察你是否理解 “同步/异步” 和 “阻塞/非阻塞” 的本质区别,以及 I/O 操作的两个阶段(等待数据、拷贝数据)。这块答得好不好,一下就能看出你是真懂还是临时背的。
  3. 工程联系能力:能否把 IO 模型和实际中间件对应起来,比如 Redis 为什么快、Nginx 为什么能扛高并发、Java NIO 底层用的是哪种模型。能联想到这一层,面试官会觉得你是真用过、真思考过。

核心答案

操作系统的 I/O 模型一共有 5 种,按同步/异步分类如下:

IO 模型 第一阶段(等数据) 第二阶段(拷数据) 同步/异步
阻塞 IO(BIO) 阻塞 阻塞 同步
非阻塞 IO(NIO) 非阻塞(轮询) 阻塞 同步
IO 多路复用 阻塞(可监听多个) 阻塞 同步
信号驱动 IO 非阻塞(等信号) 阻塞 同步
异步 IO(AIO) 不阻塞 不阻塞 异步

前 4 种都是同步 IO,只有最后一种 AIO 才是真正的异步 IO。判断标准就看数据拷贝阶段会不会阻塞线程,只要第二阶段还阻塞,它就是同步的。

深度解析

一、先搞清楚一次 IO 到底发生了什么

理解 5 种模型的前提,是先搞明白一次网络 read 操作的完整过程。应用程序发起一次读操作,内核要干两件事:

IO 两阶段流程

  • 阶段一:等待数据就绪。对于网络 IO 来说,数据要经过网卡 → 内核协议栈 → 内核接收缓冲区,这个过程是异步发生的,应用程序根本插不上手,只能等内核把数据准备好。
  • 阶段二:数据拷贝。数据到了内核缓冲区后,还得从内核空间拷贝到用户空间,这一步是 CPU 干的活,应用程序照样得等。

这 5 种 IO 模型,差别就在于 怎么处理这两个阶段:是傻等、轮询、等通知,还是交给内核全包。

IO 等数据和拷数据

二、5 种 IO 模型逐一拆解

IO 模型阻塞点

1. 阻塞 IO(Blocking IO)

最朴素的模型,也是默认行为。应用程序调用 recvfrom(),整个流程都阻塞:

阻塞 IO 流程

特点:全程阻塞,一个连接占一个线程。Java 最早的 ServerSocket.accept() 就是这种模型,所以传统 BIO 服务端要用线程池来支撑多个连接,连接数一上去,线程数也跟着爆炸。

2. 非阻塞 IO(Non-blocking IO)

把 socket 设置成 O_NONBLOCK,调用 recvfrom() 立刻返回:

  • 数据没来:返回 EWOULDBLOCK 错误码
  • 数据来了:才真正去拷贝

应用程序通常会写一个 while 循环不断轮询:


十八、Dubbo 面试题

共 12 题。

题目清单

  1. Dubbo 的整体架构是怎样的?
  2. Dubbo 服务发现与路由的概念有什么不同?
  3. Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?
  4. 为什么 RPC 要比 HTTP 更快?
  5. Dubbo 如何实现像本地方法一样调用远程方法的?
  6. Dubbo 的服务调用过程是什么样的?
  7. 什么是 Dubbo 的优雅停机,怎么实现的?
  8. Dubbo 支持哪些序列化方式?
  9. Dubbo 支持哪些负载均衡策略?
  10. Dubbo 支持哪些调用协议?
  11. 有用过 Dubbo 的异步调用吗?
  12. Dubbo 的缓存机制了解吗?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. Dubbo 的整体架构是怎样的?

面试考察点

  1. 全局视角:面试官不仅仅想知道你能不能画出那张经典的架构图,更是想看你有没有 “上帝视角”——能不能从整体上理解一个 RPC 框架的设计思路,而不是只会 @Reference 调用一下就完事了。
  2. 角色划分理解:考察你是否清楚 Provider、Consumer、Registry、Monitor 这几个核心角色的职责边界,以及它们之间的协作关系。说白了,Dubbo 的设计哲学就藏在这些角色的拆分里。
  3. 架构设计思维:面试官还想知道你是否理解 Dubbo 为什么要这么设计,比如为什么要有注册中心、为什么要用 SPI 机制、为什么要把协议和序列化做成可插拔的。这些才是真正体现架构思维的地方。

核心答案

Dubbo 的整体架构可以用 5 个核心角色 + 分层设计 来概括。

先说 5 个核心角色:

角色 职责 一句话理解
Provider 暴露服务,注册到注册中心 服务的提供方
Consumer 订阅服务,从注册中心获取地址 服务的消费方
Registry 服务注册与发现 电话簿,谁提供了什么服务都在这查
Monitor 统计服务调用数据 记录谁调了谁,调了多少次,耗时多久
Container 服务运行容器 负责启动、加载、运行 Provider

再说分层设计,Dubbo 把自身的功能拆成了三大层十小层,每一层只做一件事,层与层之间通过 SPI 机制解耦。这种设计让 Dubbo 极其灵活,比如你想换一个注册中心(从 ZooKeeper 换成 Nacos),只需要改配置就行,其他层完全不受影响。

深度解析

一、整体架构图

先看这张经典的架构图,Dubbo 官方文档里也有,但我会加上我自己的理解来讲:

整体的工作流程可以这样理解:

  • 注册阶段:Provider 启动后,把自己提供的服务信息(接口名、版本号、IP 地址、端口等)注册到 Registry。就好比一家餐厅开业了,去美团上登记一下自己的店名和地址。
  • 发现阶段:Consumer 启动后,向 Registry 订阅自己需要的服务。Registry 会把可用的 Provider 地址列表推送给 Consumer。这就像你在美团上搜 “火锅店”,平台把附近的店都列出来给你。
  • 调用阶段:Consumer 拿到地址列表后,根据负载均衡策略(随机、轮询、权重等)选一个 Provider,直接发起 RPC 调用。注意,调用过程不经过注册中心,是点对点的,注册中心挂了也不影响已经发现的服务的调用。
  • 监控阶段:Consumer 和 Provider 会异步地把调用次数、耗时等信息上报给 Monitor,用于运维分析和性能优化。

2. Dubbo 服务发现与路由的概念有什么不同?

面试考察点

  1. 基础概念区分:这题先看你能不能分清 Dubbo 调用链路里的 “找服务” 和 “选服务”。
  2. 调用流程理解:注册中心、目录、路由、负载均衡这些词不能只会背,还得知道它们在一次 RPC 调用里各自站在哪一段。
  3. 生产实践意识:服务发现管的是可用实例感知,路由更多用来做灰度、标签、机房、版本、环境隔离这些服务治理。

核心答案

可以先这么答:服务发现回答 “现在有哪些可用服务实例”,路由回答 “这次请求能用其中哪些实例”。

概念 解决的问题 典型输入 典型输出 在调用链路中的位置
服务发现 找到某个接口对应的提供者列表 服务名、接口名、分组、版本、注册中心数据 全量或增量的 Invoker 列表 更靠前,先拿到候选实例
路由 按规则过滤候选实例 候选 Invoker、调用上下文、路由规则 被筛选后的 Invoker 列表 服务发现之后,负载均衡之前

所以别把路由简单理解成服务发现的一部分。它俩经常一起出现,但干的活不一样。

深度解析

一、服务发现:先知道 “去哪找”

Dubbo 的服务发现,说白了就是消费者通过注册中心感知服务提供者的变化。服务提供者启动后,会把自己的地址、接口、版本、分组、元数据等信息注册到注册中心;消费者订阅对应服务,注册中心再把服务实例列表推给消费者。

常见注册中心有 Zookeeper、Nacos、Consul 等。消费者拿到的不是一个写死的地址,而是一组会变化的服务实例。provider 下线、扩容、重启,或者网络有波动,消费者侧都可以通过订阅机制感知到。

二、路由:再判断 “本次能不能调它”

路由发生在已经发现实例之后,它会在这批实例上继续按规则筛一遍。

比如现在发现了 10 个 provider,但当前请求来自上海机房,那就只调用上海机房的 provider;当前用户命中了灰度规则,那就只调用带有 gray=true 标签的 provider;当前消费者是 v2 版本,那就只访问支持 v2 的 provider。

这些场景问的不是 “有没有服务”,而是 “这次调用应该走哪一批服务”。

Dubbo 服务发现与路由流程

这条顺序要记住:服务发现先给候选集,路由把候选集筛小,负载均衡再从剩下的实例里挑一个。

三、路由和负载均衡也别混

这里很多人会顺手混在一起说:服务发现、路由、负载均衡,好像都在 “选 provider”。但它们其实是三层动作。

阶段 重点 举例
服务发现 发现候选实例 这个接口目前有 10 台 provider
路由 过滤候选实例 只允许调用上海机房的 4 台
负载均衡 从剩余实例中选 1 台 按权重、随机、一致性哈希选中某一台

面试时如果能把这三个层次拆开说,面试官一般就知道你不是只背了几个 Dubbo 名词,而是真的理解调用治理这条链路。

3. Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?

面试考察点

  1. SPI 概念理解:面试官首先想确认你是否知道 SPI 是什么、解决什么问题。如果你连 “Service Provider Interface” 的全称都说不出来,那基本就凉了。
  2. JDK SPI 的局限性:面试官想看你是否踩过 JDK SPI 的坑——全量加载导致资源浪费、没有 IOC 和 AOP 支持、配置方式原始。如果你能说出 “JDK SPI 会一次性加载所有实现类,即使你只需要其中一个”,面试官会眼前一亮。
  3. Dubbo SPI 的增强设计:这道题的终极考察点——你是否理解 Dubbo 为什么不用 JDK 原生 SPI,而是自己造了一套。能说出按需加载、依赖注入、自适应扩展、自动激活这几个增强点,说明你是真的读过源码。

核心答案

先说结论:Dubbo SPI 是对 JDK SPI 的增强版,核心区别可以一张表说清楚:

特性 JDK SPI Dubbo SPI
配置文件路径 META-INF/services/ META-INF/dubbo/
配置格式 全限定类名,每行一个 key=value 格式,如 dubbo=org.apache.dubbo...
加载方式 全量加载所有实现类 按需加载,通过 key 获取指定实现
依赖注入(IOC) 不支持 支持,自动注入依赖组件
AOP 增强 不支持 支持 Wrapper 包装机制
自适应扩展 不支持 @Adaptive 注解,运行时动态选择实现
自动激活 不支持 @Activate 注解,条件自动加载
获取扩展点 ServiceLoader.load() ExtensionLoader.getExtensionLoader()

一句话概括:**JDK SPI 是 “一把梭全加载”,Dubbo SPI 是 “要啥加载啥,还能自动装配”**。

深度解析

一、JDK SPI 的工作方式

先回顾一下 JDK SPI 怎么用的,后面对比才更清晰。

SPI 的全称是 Service Provider Interface,是 JDK 提供的一种服务发现机制。本质就是:接口定义在 A 模块,实现在 B 模块,运行时通过配置文件找到实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 1. 定义接口
public interface Serialization {
byte[] serialize(Object obj);
Object deserialize(byte[] data);
}

// 2. 提供实现类
public class JsonSerialization implements Serialization {
@Override
public byte[] serialize(Object obj) {
// JSON 序列化实现
}
@Override
public Object deserialize(byte[] data) {
// JSON 反序列化实现
}
}

public class HessianSerialization implements Serialization {
@Override
public byte[] serialize(Object obj) {
// Hessian 序列化实现
}
@Override
public Object deserialize(byte[] data) {
// Hessian 反序列化实现
}
}

// 3. 在 META-INF/services/ 下创建配置文件
// 文件名:com.example.Serialization
// 文件内容(全限定类名,每行一个):
// com.example.JsonSerialization
// com.example.HessianSerialization

// 4. 使用 ServiceLoader 加载
ServiceLoader<Serialization> loader = ServiceLoader.load(Serialization.class);
for (Serialization ser : loader) {
// 问题是:所有实现类都会被实例化,即使你只想用 JsonSerialization
System.out.println(ser.getClass().getName());
}

4. 为什么 RPC 要比 HTTP 更快?

面试考察点

  1. 协议理解深度:面试官不仅仅是想知道 RPC 比 HTTP 快,更是想知道你是否理解 HTTP/1.1 的协议开销(Header 冗余、文本格式编码),以及 RPC 框架在协议层做了哪些精简。
  2. 序列化机制认知:考察你是否清楚 JSON/XML 这类文本序列化方式与 Protobuf、Hessian 等二进制序列化的性能差距,以及背后的原因。
  3. 连接管理意识:看你是否了解长连接复用、连接池管理对性能的影响,以及 HTTP 短连接模型的固有开销。
  4. 工程辨析能力:一个加分项——能否客观地说出 “RPC 不一定总是比 HTTP 快”,比如 HTTP/2 和 gRPC 的出现已经大幅缩小了这个差距。

核心答案

先说结论:RPC 之所以比传统 HTTP(特指 HTTP/1.1 + JSON)更快,核心原因有 4 个——

维度 HTTP/1.1 + JSON RPC(Dubbo/gRPC 等) 性能差距
协议格式 文本协议,Header 臃肿 自定义二进制协议,精简高效 报文体积差距 3~10 倍
序列化方式 JSON(文本) Protobuf/Hessian(二进制) 序列化速度差距 5~20 倍
连接模型 短连接为主 / Keep-Alive 有限复用 长连接 + 连接池 减少 TCP 握手开销
网络模型 BIO/NIO 混用 NIO/Epoll 优化的 Netty 高并发吞吐量差距明显

下面一个个拆开讲。

深度解析

一、协议格式:报文 “臃肿” vs “精瘦”

HTTP/1.1 的报文头是纯文本的,每次请求都要携带一大堆 Header 信息。来看个直观的例子:

  • HTTP/1.1 的 Header 是文本格式,每次请求都要带上 Content-Type、User-Agent、Accept 等一堆跟业务无关的信息。这些 Header 在内部服务调用中完全多余,但协议规定必须解析。
  • Dubbo 的协议头只有固定的 16 字节(Magic Number + Flag + Status + Request ID + Data Length),极其精简。没有废话,全是干货。
  • 实际场景中,一个简单的 HTTP 接口调用,Header 可能就占了 300~800 字节,而 RPC 协议头只有 16 字节。在高频调用下,这个差距会被无限放大。
二、序列化:文本 vs 二进制

这是性能差距最大的一块。

HTTP 最常用的序列化格式是 JSON,而 RPC 框架通常使用二进制序列化(Protobuf、Hessian、Kryo 等)。两者差距在哪?


十九、RAG 面试题

共 17 题。

题目清单

  1. 什么是 RAG?为什么需要 RAG?
  2. RAG 的完整流程是什么?
  3. 为什么 RAG 要用向量数据库?其他数据库不行吗?
  4. Embedding 是什么?1536 维什么意思?
  5. 你是怎么做分块的?块大小怎么选的?
  6. 固定大小分块有什么问题?
  7. RAG vs 微调 vs 提示工程,什么时候用哪个?
  8. 混合检索是什么?为什么要用混合检索而不是纯向量检索?
  9. 什么是重排序(Rerank)?为什么检索之后还要重排序?
  10. 递归分块和语义分块的区别?
  11. RAG 系统常见的失败模式有哪些?检索质量差怎么排查和优化?
  12. 什么是 RAG 查询改写(Query Rewriting)?它怎么提升检索效果的?
  13. RAG 中的幻觉问题怎么处理?
  14. 常用向量数据库怎么选型?
  15. 什么是假设性文档嵌入( HyDE)?它的工作原理是什么?
  16. 如何评估 RAG 系统的效果?常用的评估指标有哪些?
  17. 什么是 Self-RAG?它如何通过自我反思提升回答质量?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 什么是 RAG?为什么需要 RAG?

面试考察点

  1. 概念理解:面试官不仅仅是想听你背出 RAG 的全称,更想知道你是否理解它的本质——RAG 不是模型自身的能力增强,而是一种 “外挂知识库” 的架构模式。
  2. 问题意识:你能不能说清楚大模型到底有哪些硬伤?为什么不能直接让它回答所有问题?这块考察的是你对 LLM 局限性的理解深度。
  3. 方案选型思维:知道有哪些解决大模型局限性的方案(RAG、微调、长上下文等),能说清楚为什么选 RAG,而不是上来就 “我会用 RAG”。

核心答案

RAG(Retrieval-Augmented Generation,检索增强生成) 的核心思路就一句话:让大模型在回答问题之前,先去外部知识库里检索相关资料,然后基于这些资料来生成回答。

打个比方,大模型就像一个学生参加开卷考试——与其死记硬背所有知识(预训练),不如带着参考书进场,需要的时候翻一翻。RAG 就是给大模型配的这本 “参考书”。

那为什么需要 RAG? 直接让大模型回答不行吗?还真不太行,因为大模型有几个绕不开的硬伤:

大模型的硬伤 具体表现 RAG 如何解决
幻觉问题 一本正经地胡说八道,编造不存在的事实 基于检索到的真实文档生成回答,有据可依
知识截止 训练数据有截止日期,不知道最新的信息 知识库可以随时更新,突破时间限制
领域知识不足 对企业内部文档、专业知识了解有限 接入企业私有数据,补齐领域短板
无法溯源 不知道回答的依据是什么,无法验证 可以追溯引用来源,提供文档出处

深度解析

一、大模型为什么 “不够用”?

先聊聊大模型的几个核心痛点,这样你才能真正理解 RAG 的价值。

痛点一:幻觉(Hallucination)

这是大模型最被诟病的问题。大模型的本质是 “接龙”——基于上文预测下一个最可能的 Token。它并不真正 “理解” 事实,只是在做概率预测。所以当它不确定的时候,它会编一个看起来很合理的答案,而不是老老实实说 “我不知道”。

这块幻觉问题当年坑了不少人。早期很多团队直接拿 GPT-3.5 做企业客服,结果模型一本正经地给客户推荐了不存在的产品,场面一度非常尴尬。

痛点二:知识截止(Knowledge Cutoff)

大模型的知识来源于训练数据,训练完就 “定格” 了。你问它 2025 年发生的新闻事件,它要么不知道,要么靠编。对于需要实时信息的场景(股价、新闻、政策更新),裸用大模型基本不可行。

痛点三:私有知识缺失

大模型的训练数据是公开的互联网语料。你们公司的内部文档、产品手册、客户 FAQ,它统统没见过。你让它回答 “我们公司 XX 产品的退货政策是什么”,它只能瞎编。

痛点四:成本与可控性

微调模型来注入新知识?可以,但成本高、周期长,而且每次知识更新都得重新训练。对于频繁变化的知识场景,这显然不现实。

二、RAG 的核心流程

2. RAG 的完整流程是什么?

面试考察点

  1. 流程完整性:能不能把 RAG 从离线建库到在线生成完整走一遍?中间有遗漏环节(比如漏掉 Rerank)会被扣分。
  2. 每个环节的深度理解:文档解析有哪些难点?Chunk 策略怎么选?Embedding 模型选型依据是什么?向量检索的原理了解吗?每个环节面试官都可能追问细节。
  3. 生产实战经验:你搭建 RAG 系统的时候踩过什么坑?知道怎么优化效果吗?能结合实际项目讲才最有说服力。

核心答案

RAG 的完整流程分为两大阶段、六个核心环节:

1
2
离线阶段:文档解析 → 文本切分 → 向量化 → 存入向量库
在线阶段:向量检索 → 重排序 → LLM 生成

下面我把每个环节掰开揉碎地讲一遍。

深度解析

一、离线阶段:知识库构建

离线阶段的目标是把各种非结构化文档变成可检索的向量数据。这一步是整个 RAG 的地基,地基没打好,后面检索再怎么优化都是白搭。

1. 文档解析(Document Parsing)

文档解析看着简单,实际是整个流程中最容易被低估的环节。它的任务是把 PDF、Word、HTML、Excel、PPT 等各种格式的文档,转换成结构化的纯文本。

常见的坑:

  • 表格数据丢失:很多解析器遇到表格直接丢弃或变成乱序文本。表格里的数据往往是最有价值的(比如产品参数、定价信息),丢了就检索不到
  • 图片中的文字:要不要做 OCR?做了准确率够不够?不做就丢失信息。这块需要根据业务场景权衡
  • 公式提取:学术论文、技术文档中的数学公式,大部分解析器处理得不好
  • 多栏排版:双栏论文解析后文字顺序经常错乱

生产环境推荐:

  • 轻量级场景用 Apache Tika(Java 生态最成熟的文档解析库)就够了
  • 复杂文档(特别是含表格、图片、公式的)建议用 LlamaParse、Marker 等专业工具
  • 国内也有一些不错的方案,比如 MinerU(开源的文档解析工具,对中文 PDF 支持较好)
2. 文本切分(Chunking)

文本切分是 RAG 效果的 “隐形杀手”——切得不好,后面所有环节都在为它买单。

常见的切分策略:

  • 固定长度切分:按字符数或 Token 数切割,最简单但最粗暴。容易把一个完整的句子或段落从中间截断,破坏语义完整性
  • 递归字符切分:按分隔符的优先级逐级尝试切分(先按 \n\n,再按 \n,再按空格),尽量保留段落结构。Spring AI 的 TokenTextSplitter 和 LangChain4j 的 DocumentSplitters 都支持这种模式
  • 语义切分:用 Embedding 模型计算相邻文本的语义相似度,在语义断裂处切分。效果最好,但需要额外的模型调用,成本较高
  • 基于文档结构的切分:利用文档的标题层级、段落标记等结构信息来切分。对 Markdown、HTML 这类结构化文档效果很好

实战经验值:

3. 为什么 RAG 要用向量数据库?其他数据库不行吗?

面试考察点

  1. 语义检索的本质理解:你到底懂不懂 RAG 的检索环节是在做什么?它不是在做关键词匹配,而是在做 “语义相似度” 计算——这是选用向量数据库的根本原因。
  2. 数据库选型的判断力:能不能区分向量数据库、关系型数据库、搜索引擎各自的适用场景?面试官想看的是你的技术选型思维,而不是 “大家都用所以我也用”。
  3. 对行业融合趋势的感知:2025-2026 年,传统数据库和搜索引擎都在往向量能力靠拢(如 Elasticsearch 原生支持 kNN、PostgreSQL 有 pgvector 插件)。能不能聊到这个趋势,是区分 “了解” 和 “关注前沿” 的标志。

核心答案

一句话:RAG 要做的是语义检索,不是关键词匹配。向量数据库是专门为语义相似度搜索设计的,传统数据库做不到这件事。

打个比方:用户问 “怎么退货”,传统数据库会去搜包含 “怎么” 和 “退货” 这两个关键词的文档;但如果你的知识库里写的是 “退换货政策” 或 “商品返还流程”,传统数据库就搜不到了。而向量数据库能把 “怎么退货” 和 “退换货政策” 理解为语义相近的内容,即使它们没有一个字相同。

这就是根本原因 —— RAG 的检索核心是 “找到语义相关的文档”,而向量数据库天生就是干这个的。

深度解析

一、不同数据库的检索方式对比

先看一张对比图,把几种数据库的检索方式放在一起,区别一目了然:

  • 关系型数据库(MySQL 等):基于精确匹配或模糊匹配(LIKE)。它要的是 “字面上一样”,哪怕意思完全相同,只要字不一样就搜不到。适合结构化数据的 CRUD 操作,不适合语义检索
  • 搜索引擎(Elasticsearch):基于倒排索引 + BM25 算法做关键词检索。比关系型数据库强不少,支持分词、同义词扩展、权重打分,但它本质上还是在做关键词匹配,不理解 “语义”。搜 “怎么退货” 找不到 “商品返还流程”,因为它们共享的关键词太少
  • 向量数据库(Milvus 等):把文本编码成高维向量,通过计算向量之间的距离(余弦相似度、欧氏距离等)来判断语义相似度。它关注的是 “意思相近”,而不是 “字面一样”。这正是 RAG 需要的能力
二、为什么传统数据库搞不定语义检索?

关键在于 “怎么表示语义” 这个问题。

传统数据库存储的是原始文本。你说 “怎么退货”,它就在文档里找 “怎么” 和 “退货” 这几个字。它不理解这句话的意思。

向量数据库的思路完全不同。它先用 Embedding 模型把文本编码成一个高维向量(比如 1536 维的浮点数数组)。这个向量就是这段文本的 “语义指纹”——语义相近的文本,它们的向量在空间中的距离也相近。

  • “怎么退货”、”退换货政策”、”商品返还流程” 这三句话字面上差异很大,但意思相近,所以它们的向量在空间中距离很近。向量数据库能轻松把它们关联起来
  • “北京天气” 和 “Python 入门教程” 语义完全不相关,向量距离远,自然不会被一起检索出来

4. Embedding 是什么?1536 维什么意思?

面试考察点

  1. 概念理解深度:不仅仅是背定义,而是能不能用自己的话解释清楚——文本是怎么变成数字的?这些数字代表了什么?为什么能用数字的 “距离” 来衡量语义的 “远近”?
  2. 维度的直觉理解:1536 维到底什么意思?为什么不是 10 维或者 10 万维?维度和语义表达能力之间是什么关系?这块答不清楚,说明只是记了个数字。
  3. 工程实践意识:实际用 Embedding 的时候怎么选模型?维度对存储和计算有什么影响?面试官想知道你有没有在真实项目里踩过坑。

核心答案

Embedding(向量嵌入) 就是把一段文本转换成一个浮点数数组(向量),这个数组是这段文本的 “语义指纹”。

比如你输入 “今天天气真好”,Embedding 模型会输出类似这样的结果:

1
[0.012, -0.034, 0.567, -0.189, 0.423, ..., 0.078]  ← 共 1536 个浮点数

1536 维的意思是:这个浮点数数组有 1536 个元素。每个元素是一个 float 值,比如 0.012、-0.034。这 1536 个数共同编码了这段文本的语义信息。

核心逻辑:语义相近的文本,它们的向量在高维空间中距离也相近。 所以你可以通过计算两个向量的距离(余弦相似度)来判断两段文本在语义上有多相似——这就是 RAG 中向量检索的底层原理。

深度解析

一、从文本到向量——到底发生了什么?

很多人知道 “文本变向量”,但具体怎么变的?来,我们一步步拆。

  • 输入:一段文本(比如 “怎么退货”),送入 Embedding 模型
  • 模型内部:文本先被切分成 Token 序列,然后经过 Transformer 编码器的多层注意力机制,逐层提取语义特征。这个过程是预训练好的,你不需要关心细节,只要调用 API 就行
  • 输出:一个固定长度的浮点数数组。用 OpenAI 的 text-embedding-3-small 模型,输出就是 1536 个浮点数

注意,你不需要手动去做这些操作。在实际开发中,不管是 Spring AI 还是 LangChain4j,调用 Embedding 模型就是一行代码的事。但理解这个过程有助于你明白为什么维度越高语义表达能力越强——因为更多的维度意味着模型能编码更多的语义特征。

二、1536 维——为什么是这个数字?

说实话,1536 这个数字本身没有特别的含义,它不是什么 “最优解”,而是 OpenAI 在设计 text-embedding-ada-002 模型时选定的一个参数。后来 text-embedding-3-small 延续了这个维度。

但它背后的逻辑是有道理的:


二十、AI Agent 面试题

共 12 题。

题目清单

  1. 什么是 Agent?与大模型有什么本质不同?
  2. Agent 的基本架构由哪些核心组件构成?
  3. Workflow、Agent、Tools 这三个的概念和区别介绍一下?
  4. ReAct、Plan-and-Execute、Reflection 三种范式有什么核心区别?实际项目怎么选型?
  5. 请介绍一下 AI Agent 的记忆机制?实际开发中怎么设计记忆模块?
  6. Agent 的长短期记忆系统怎么做的?记忆怎么存?粒度是多少?怎么用?
  7. Agent 推理模式有哪些?ReAct 具体是怎么实现的?
  8. Agent 记忆压缩通常有哪些方法?
  9. 什么是 Multi-Agent?Single-Agent 和 Multi-Agent 的设计方案?
  10. 讲讲 Agent 的反思机制?为什么要用反思?具体怎么实现?
  11. 复杂任务怎么做的任务拆分?为什么要拆分?效果如何提升?
  12. 如何设计多 Agent 的协作与动态切换机制?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 什么是 Agent?与大模型有什么本质不同?

面试考察点

  1. 概念边界是否清晰:面试官最怕听到的就是 “Agent 就是大模型加个对话框” 这种回答。他真正想确认的是,你能不能分清 LLM 是 “大脑”,而 Agent 是 “大脑 + 感知 + 规划 + 记忆 + 工具 + 反思” 这一套完整的东西。
  2. 架构思维:Agent 的核心循环(感知 → 规划 → 执行 → 反思)能不能讲清楚?ReAct、Plan-and-Execute、Reflexion 这几个主流范式能不能区分开?
  3. 工程落地意识:有没有真正动手搭过 Agent?工具调用(Function Calling / Tool Use)到底怎么实现的知不知道?踩没踩过 Agent “跑飞了”、”死循环”、”幻觉调用一个根本不存在的工具” 这类坑?

核心答案

先给一句话抓重点:

大模型(LLM)是语言能力的基座,Agent 是基于 LLM 搭出来的、能自主完成多步骤任务的智能体系统。

打个比方,LLM 是 “大脑”,Agent 是 “大脑 + 眼睛 + 手脚 + 工具箱 + 记忆本 + 反思能力”。前者 “能说”,后者 “能做”。

业界公认的 Agent 公式(Lilian Weng 给出的定义):

1
Agent = LLM(大脑) + Planning(规划) + Memory(记忆) + Tools(工具使用) + Reflection(反思,可选)

两者的区别看这张表就够了:

维度 大模型(LLM) AI Agent
定位 语言理解与生成的基座模型 基于 LLM 的智能体系统
能力边界 单次输入 → 单次输出 多步骤、跨工具、跨时间地完成任务
交互方式 一问一答,被动响应 自主决策,主动调用工具与环境交互
状态管理 无状态(除上下文窗口外) 有短期记忆 + 长期记忆
核心组件 Transformer 神经网络 LLM + 规划器 + 记忆模块 + 工具集 + 反思循环
典型例子 GPT-4o、Claude 4.6、Qwen3、DeepSeek-V3 Cursor、Manus、Devin、AutoGPT、Claude Code
AGI 阶段 L2(推理者) L3(Agent)

2025 年大家都叫它 “Agent 元年”。最直观的感受是,AI 从 “能聊天的助手” 变成了 “能真干活的同事”,像 Manus、Devin、Claude Code 这些工具开始真正出现在生产环境里了。

深度解析

一、Agent 的核心循环

先看一张 Agent 的工作流程图,把整个生命周期串起来:

Agent 核心循环:感知、规划、执行、反思

图里画的就是 Agent 的核心循环,分四步走:感知 → 规划 → 执行 → 反思。

2. Agent 的基本架构由哪些核心组件构成?

面试考察点

  1. 架构认知的系统性:面试官想知道你是不是把 Agent 当成 “聊天机器人 + 函数调用” 的简单玩意儿。它其实是一套完整的认知架构,你得把大脑(LLM)、规划、记忆、工具这几块讲清楚,还能说出它们怎么配合。
  2. 理论与实践结合:能不能结合主流框架(Spring AI、LangChain4j、LangChain)说说这些组件怎么落地?停在概念层和能写出代码,完全是两个段位。
  3. 演进意识:Agent 这块 2024 到 2026 年变化太快了,从单 Agent 到 Multi-Agent,从手工编排到 ReAct、Plan-and-Execute、Reflexion 各种范式冒出来。能聊到演进趋势,说明你一直跟得上。

核心答案

Agent 架构最经典的定义,来自 OpenAI 前研究主管 Lilian Weng 提出的公式:

Agent = LLM(大脑)+ Planning(规划)+ Memory(记忆)+ Tool Use(工具使用)

谷歌 2025 年的 Agent 白皮书把它简化成 “模型 + 工具 + 编排层(Orchestration)” 三件套,其实就是把规划、记忆、循环调度都塞进编排层里。

我建议面试时按 五大核心组件 来答,清楚,也不容易漏:

组件 作用 类比
LLM(大脑) 核心推理与决策引擎 人的大脑
Planning(规划) 任务拆解、推理决策、反思 人的思考过程
Memory(记忆) 存储上下文与历史经验 短期记忆 + 长期记忆
Tools(工具) 与外部世界交互 人的手脚和工具
Action / 感知(执行与输入) 接收输入、执行动作 眼睛看 + 手去做

一句话总结:Agent 就是以 LLM 当大脑,靠 “感知 → 规划 → 行动 → 反思” 这个循环,配合记忆和工具去把复杂任务办了的系统。

深度解析

一、Agent 的整体架构

先上一张整体架构图,把组件之间的关系串起来:

AI Agent 整体架构图

这张图把 Agent 的核心循环讲明白了,我拆开来说:

  • 感知层:接收用户输入或环境信号,包括文本、图像、语音等多模态输入。这是 Agent 和外界打交道的第一道关
  • LLM 大脑:整个系统的决策中枢,负责理解意图、生成下一步行动方案。注意它不是单次问答,而是在一个循环里不断被调用
  • 规划层:Agent 和普通 Chatbot 的区别就在这里。它把一个复杂目标拆成多个可执行步骤,再决定每一步做什么、调用什么工具
  • 记忆层:分短期和长期两块。短期记忆管当前对话上下文,长期记忆把历史经验和知识存进向量库
  • 工具层:Agent 的 “手脚”,通过 Function Calling 调用各种外部能力,比如查数据库、调 API、跑代码、检索知识库
  • 执行层:真正去调用工具、产生副作用,再把结果回传给 LLM 进入下一轮循环

3. Workflow、Agent、Tools 这三个的概念和区别介绍一下?

面试考察点

  1. 概念辨析:面试官想看你能不能把 Tools、Workflow、Agent 三者的边界说清楚。很多人一上来就混着用这三个词,Anthropic 在 2024 年 12 月那篇《Building Effective Agents》里给了业界公认的定义,先把这个记牢。
  2. 架构选型意识:知道区别只是入门。给你一个具体业务场景,你能选出合适的形态吗?”什么时候用 Workflow,什么时候必须上 Agent”,这才是面试官真正想听的。
  3. 工程实践:在 Java 生态里(Spring AI、LangChain4j),这三种东西分别怎么落地?有没有动手写过?
  4. 前沿认知:Anthropic 提的 “简单胜于复杂” 原则你认同吗?2025 年 Spring AI 1.0 把这些概念正式引入了 JVM 生态,你关注过吗?

核心答案

这三个东西不是并列关系,而是层级递进的,一句话概括:

Tools 是积木,Workflow 是按图纸拼好的流水线,Agent 是会自己看图纸自己拼的工人。

维度 Tools(工具) Workflow(工作流) Agent(智能体)
本质 LLM 可调用的离散函数 人预先编排的固定流程 LLM 自主驱动的循环系统
决策方 调用者(人或 LLM) 人(开发者写死路径) LLM(动态决定下一步)
可控性 单步可控 高(路径固定) 低(路径不可预测)
灵活性 低 中 高
典型形态 查天气、算数、查 DB RAG 管线、路由分发 ReAct 循环、Plan-and-Execute
Java 框架落地 @Tool 注解 ChatClient 链式 API / LangGraph 节点 Tool Calling + 自主循环

Anthropic 给的核心建议很朴素:先从最简单的方案开始,能用 Workflow 解决的就别上 Agent。

深度解析

一、Tools(工具)—— 最小积木

Tools 是三者里最底层的概念,说白了就是一个可以让 LLM 调用的函数。它本身没 “智能”,就是一个被动的能力单元,查个数据库、调个 API、读个文件,都是它干的活。

AI Tool 工具调用流程

图上能看出来,Tool 的角色很纯粹,只负责 “执行具体动作”,至于 “什么时候调、调几次、调用结果怎么处理”,那是上层 Workflow 或 Agent 的事。

在 Spring AI 里,定义一个 Tool 很简单,用 @Tool 注解就够了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class WeatherTools {

@Tool(description = "查询指定城市的实时天气信息")
public String getWeather(String city) {
// 实际调用天气 API
return WeatherApi.query(city);
}

@Tool(description = "查询指定城市的未来三天天气预报")
public String getForecast(String city) {
return WeatherApi.forecast(city, 3);
}
}

4. ReAct、Plan-and-Execute、Reflection 三种范式有什么核心区别?实际项目怎么选型?

面试考察点

  1. 范式理解深度:面试官想知道你是不是真理解这三种范式背后的 推理机制差异,不是只会说 “ReAct 就是边想边做”。这三种范式对应的是不同的任务假设,确定性任务、探索性任务、需要自我纠错的任务,能说清楚这个才算入门。
  2. 工程选型能力:能不能根据任务特点(步骤数、确定性、成本敏感度、错误容忍度)选合适的范式?这块是区分 “会用 LangChain4j” 和 “能独立设计 Agent 架构” 的分水岭。
  3. 组合应用意识:知不知道这三种范式 不是非此即彼,可以组合使用?生产级 Agent 一般都是 “Plan-and-Execute 做骨架 + ReAct 做执行 + Reflection 做兜底”。

核心答案

先给结论,一句话概括这三种范式的本质区别:

范式 核心机制 关键词 一句话定位
ReAct Reasoning + Acting 交替循环 “走一步看一步” 边想边干,实时反馈
Plan-and-Execute 先规划全局,再分步执行 “先画地图再走路” 全局视野,执行高效
Reflection 执行后自我反思 + 纠错重试 “做完复盘再做” 自我改进,提升准确率

选型核心逻辑:

  • 任务路径不确定、需要边做边探索 → ReAct
  • 任务步骤多且复杂、需要全局规划 → Plan-and-Execute
  • 任务对准确率要求极高、允许多次重试 → Reflection(通常作为增强层叠加在前面两者之上)

实际项目里,这三种范式往往组合着用,不是单选题。

深度解析

一、ReAct 范式:边想边做的 “侦探”

ReAct(Reasoning and Acting)是最早的 Agent 范式之一,2022 年由 Yao Shunyu 等人提出。它让 LLM 交替输出 Thought(思考)→ Action(动作)→ Observation(观察结果),形成一个推理-执行循环。

ReAct 范式工作流程

是 ReAct 的核心循环。每一次 LLM 调用,模型都要走完 “想清楚下一步干啥 → 调工具 → 看结果 → 再想” 这个闭环。几个阶段:

  • Thought 阶段:模型把推理过程写出来,算是 Chain-of-Thought(CoT)的延伸
  • Action 阶段:模型决定调哪个工具、传什么参数
  • Observation 阶段:工具结果回灌给模型,作为下一轮推理的输入

ReAct 的优势:

  • 实时适应性强:每一步都基于最新观察做决策,适合探索性任务
  • 实现简单:一个 Prompt 模板 + 工具循环就能跑起来
  • debug 友好:Thought 链路清晰可见,容易定位问题

ReAct 的短板:


二十一、Netty 面试题

共 10 题。

题目清单

  1. 为什么 Netty 适合做网络编程?
  2. Netty 性能好的原因是什么?
  3. Netty 如何解决 TCP 粘包、拆包的问题?
  4. Netty 有哪些序列化协议?
  5. Netty 的零拷贝是怎么实现的?
  6. Netty 的线程模型是怎么样的?
  7. Netty 的 ByteBuf 为什么好用?
  8. 说说 Netty 的对象池技术?
  9. 谈谈 Netty 的无锁化设计?
  10. Netty 中用了哪些设计模式?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 为什么 Netty 适合做网络编程?

面试考察点

  1. 对 NIO 痛点的理解:面试官想看的其实不是你会不会用 Netty,而是你清不清楚 JDK 原生 NIO 用起来有多痛苦。把痛点搞明白了,自然就懂 Netty 为啥能冒出来。
  2. Netty 核心优势的掌握:能不能讲清楚 Netty 在设计、性能、稳定性这些方面到底好在哪,而不是只会背一句 “Netty 是高性能 NIO 框架”。
  3. 架构设计思维:对 Reactor 线程模型、零拷贝、内存池、无锁串行化这些高性能网络编程的底层招数,有没有真懂。

核心答案

Netty 适合做网络编程,主要靠这几把 “刷子”:

优势维度 核心能力 解决的问题
设计优雅 Reactor 线程模型 简化并发编程复杂度
性能极致 零拷贝、内存池、无锁化 顶住高并发、低延迟
功能丰富 编解码器、心跳、粘包处理 不用重复造轮子
使用简单 链式 API、回调机制 降低 NIO 上手门槛
稳定性高 修复 JDK epoll bug 生产可用
生态强大 Dubbo、RocketMQ 等背书 经过大流量验证

说白了就一句:JDK NIO 的坑 Netty 帮你填了,复杂的 API 帮你封了,性能也帮你抠到家了——Java 网络编程的事实标准,就这么来的。

深度解析

一、先聊聊 JDK 原生 NIO 的那些坑

要理解 Netty 为什么厉害,得先看看 JDK 原生 NIO 用起来有多让人崩溃。

JDK 原生 NIO 的主要痛点

  • API 使用复杂:Selector、SelectionKey、ServerSocketChannel、SocketChannel 一堆类组合使用,新手上手成本极高。一段简单的 Echo 服务,用 NIO 写能写一百多行,还容易出错。
  • epoll 空轮询 bug:这是 JDK NIO 一个臭名昭著的 bug。select() 本该阻塞等待事件,但在某些 Linux 内核版本下会异常返回 0,导致线程陷入死循环空转,CPU 直接飙到 100%。JDK 至今没彻底修复,Netty 通过巧妙的检测 + 重建 Selector 把这个坑绕过去了。
  • 粘包/半包自己处理:TCP 是流式协议,不保证包边界。用 NIO 你得自己设计拆包逻辑,新手十有八九会在这里翻车。
  • ByteBuffer 难用:JDK 的 ByteBuffer 是只读 / 只写模式切换的设计(通过 flip() 切换),指针绕来绕去,而且无法动态扩容,写满了就得自己重新分配。
  • 断线重连、心跳保活:NIO 都得自己写。

我之前做的一个长连接项目,最开始用原生 NIO,光是处理 epoll 空轮询 + 粘包拆包就搞了快两周。后来换成 Netty,三天就跑通了。这就是框架的力量。

二、Netty 的核心优势
1. Reactor 线程模型:并发处理不费脑子

Netty 的核心是基于 Reactor 模式,最经典的是主从 Reactor 多线程模型:

2. Netty 性能好的原因是什么?

面试考察点

  1. NIO 原理掌握度:面试官不仅仅是想知道你 “会用” Netty,更想知道你是不是理解 IO 多路复用、Reactor 线程模型这些底层设计。如果只会调 API 而不懂为什么这么设计,说明还停留在 “工具人” 层面。
  2. 零拷贝理解深度:零拷贝是 Netty 高频考点。从操作系统的 sendfile 到用户态的 CompositeByteBuf,能讲到什么深度直接决定了你的技术段位。
  3. 工程优化意识:内存池、对象池、无锁化串行设计这些优化手段,能体现你是不是真的做过高并发场景,或者至少研究过 Netty 的源码。

核心答案

Netty 性能好靠的是好几块优化一起堆出来的,单拎任何一个点都不够。先把骨架列出来:

优化维度 核心机制 解决什么问题
IO 模型 IO 多路复用 + Reactor 主从线程模型 一个线程处理大量连接,减少线程切换开销
内存管理 PooledByteBufAllocator 内存池 + Recycler 对象池 减少 GC 压力,避免频繁分配回收
零拷贝 操作系统级(sendfile)+ 用户态级(CompositeByteBuf) 减少数据在内存中的拷贝次数
并发设计 无锁串行化 + FastThreadLocal 避免锁竞争,提升 ThreadLocal 性能
其他优化 优化的编解码、EventLoop 串行处理 减少不必要的对象创建和上下文切换

下面挨个展开。

深度解析

一、IO 多路复用 + Reactor 主从线程模型

Netty 底层基于 Java NIO,核心思想就一条——一个线程管理多个连接,把传统 BIO “一个连接一个线程” 的玩法彻底掀翻了。

  • Boss Group:专门负责接收客户端的连接(OP_ACCEPT 事件),接收完就把 Channel 扔给 Worker Group。Boss 线程通常只要 1 个就够了,因为 accept 这个动作很轻量。
  • Worker Group:负责实际的读写(OP_READ、OP_WRITE)和业务处理。每个 NioEventLoop 内部绑定一个 Selector,可以同时监听几百上千个 Channel。

关键点:每个 Channel 在它整个生命周期内,只会被同一个 NioEventLoop 处理。这个设计是后面 “无锁串行化” 的基础,等会儿讲。

为什么这比传统 BIO 快?传统模型 1 万个连接就要 1 万个线程,光是线程上下文切换就能把 CPU 跑没。Netty 用少量线程管所有连接,靠的是操作系统的 epoll(Linux)/ kqueue(Mac)/ select 这些多路复用机制。

二、零拷贝(Zero-Copy)

零拷贝这块是面试重灾区,很多候选人只会说一句 “用零拷贝”,但说不清 Netty 的零拷贝其实分两层。

1. 操作系统级零拷贝

3. Netty 如何解决 TCP 粘包、拆包的问题?

面试考察点

  1. 对 TCP 协议的理解:面试官想看你是否真的明白 TCP 是 “面向字节流” 的协议,没有消息边界这回事。很多人粘包拆包讲不清楚,根因就是对 “流” 这个概念没吃透。
  2. Netty 实战经验:考察你在实际项目中是怎么处理这个问题的——是知道有现成的解码器,还是傻乎乎自己手写拆包逻辑。
  3. 解码器原理的理解深度:如果你只知道 LengthFieldBasedFrameDecoder 这个类名,但说不出它的几个参数啥意思、底层 ByteToMessageDecoder 是怎么累积数据的,那只能算 “会用”,离 “懂原理” 还差一截。

核心答案

一句话总结:TCP 是面向流的协议,本身没有消息边界,粘包拆包是它的天然特性,不是 bug。Netty 的解法是在 pipeline 里加专门的解码器(Decoder),通过约定好的规则把字节流重新切分成一条条完整的消息。

Netty 内置了 4 种主流解码器:

解码器 切分依据 适用场景
FixedLengthFrameDecoder 固定长度 简单协议、定长报文
LineBasedFrameDecoder 换行符 \n / \r\n 文本协议
DelimiterBasedFrameDecoder 自定义分隔符 自定义文本协议
LengthFieldBasedFrameDecoder 消息头长度字段 最通用、最推荐

绝大多数业务场景,答案就一个:**LengthFieldBasedFrameDecoder**。下面展开讲。

深度解析

一、为什么会出现粘包、拆包?

这块得先想明白,不然用啥解码器都是空中楼阁。TCP 是个 “流” 式协议,它根本不知道你应用层一条 “消息” 长啥样,在它眼里全是字节。底层只管把字节可靠地、有序地送达,至于边界——对不起,不归它管。

我画个图你就懂了:

发送两条消息,接收方可能收到的四种情况

  • **写入方:**应用层一次写入的数据量,和 socket 缓冲区大小不匹配。写多了,TCP 会拆开发;写少了,多条消息可能塞在一起发。
  • **传输方:**MSS(最大报文段长度)限制、链路 MTU 限制,会把大包切成小包。
  • **读取方:**接收方应用层从缓冲区读数据不及时,多条消息在缓冲区里堆成一坨。

说到底:粘包拆包不是 bug,是 TCP 流式协议的天然产物。所以这玩意儿得在应用层解决,TCP 不会帮你解决。

二、Netty 的四种解码器

Netty 的设计很贴心——粘包拆包太常见了,干脆内置几套开箱即用的方案。它们都继承自 ByteToMessageDecoder,你只需要往 pipeline 里一加就行。

1. 固定长度:FixedLengthFrameDecoder

每条消息长度固定,比如每 100 字节算一条。

4. Netty 有哪些序列化协议?

面试考察点

  1. 扩展机制理解:面试官真正想问的是 —— Netty 本身没有绑定任何序列化协议,它是通过 Codec(编解码器)框架把序列化能力抽象出来的。你是否理解 MessageToMessageCodec、ByteToMessageDecoder 这些扩展点?
  2. 生态熟悉度:Java 圈主流的序列化方案有哪些,各自的优缺点、适用场景能不能说清楚?
  3. 选型能力:不同业务场景下该怎么选 —— 内部 RPC 用 Protobuf、和前端联调用 JSON、跨语言用 MessagePack、临时调试用 Java 原生……这是工程能力。

核心答案

先一句话划重点:Netty 本身不绑定序列化协议,它只负责把字节流通过网络传输,至于字节流里装的是什么、怎么编出来的,完全交给 Codec 体系。

下面这张表把主流方案拉出来对比:

序列化协议 Netty 内置支持 跨语言 性能 体积 典型场景
Java 原生 ✅ ObjectEncoder/ObjectDecoder ❌ 差 大 调试、内部系统
JSON (Jackson/Gson/Fastjson) ❌ 自己实现 Codec ✅ 中 偏大 HTTP API、对前端
Protobuf ✅ 全套 Encoder/Decoder ✅ 优 极小 RPC、IM、移动端
MessagePack ❌ 自实现或第三方 ✅ 优 小 替代 JSON 的二进制场景
Kryo ❌ 自己实现 Codec ❌ 优 小 Java 内部高性能 RPC
Hessian ❌ 自己实现 Codec 部分 中 中 Dubbo 早期默认(Hessian2)
JBoss Marshalling ✅ MarshallingEncoder/MarshallingDecoder ❌ 中 中 JBoss 内部
Thrift / Avro ❌ 自己实现 Codec ✅ 优 小 跨语言 RPC

Netty 开箱即用的只有三种:Java 原生、Protobuf、JBoss Marshalling。其他的都得自己写 Codec,但写起来不难——继承 MessageToMessageCodec 重写两个方法就行。

深度解析

一、Netty 的编解码架构

先理解 Netty 处理序列化的位置 —— 它是在 ChannelPipeline 里挂编解码 Handler:

Netty 编解码流程

Netty 处理序列化的关键链路拆开看:

  • 编码方向(出站):业务对象 → Encoder → ByteBuf → Socket
  • 解码方向(入站):Socket → ByteBuf → 拆包/粘包处理 → Decoder → 业务对象
  • 粘包处理是必须的:TCP 是流式协议,序列化出来的字节流必须配合 LengthFieldBasedFrameDecoder 之类的拆包器,才能正确还原一个个完整消息

二十二、分库分表面试题

共 14 题。

题目清单

  1. 什么是分库?分表?分库分表?
  2. 分库分表后会带来哪些问题?
  3. 什么是数据倾斜,会带来哪些问题?如何解决?
  4. 分区和分表有什么区别?
  5. 分表字段如何选择?
  6. 分库分表的数量为什么一般选择 2 的幂?
  7. 分表后全局 ID 如何生成?
  8. 分库分表后如何进行分页查询?
  9. 分表算法都有哪些?
  10. 分库分表的取模算法策略,怎么避免数据倾斜?
  11. 分库分表之后怎么进行 Join 操作?
  12. 分库分表中,如何预估需要分多少个库?多少张表?
  13. 分库分表后,怎么设计,可以降低数据迁移的难度?
  14. ShardingJDBC 有哪些分片策略,你用的哪个?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 什么是分库?分表?分库分表?

面试考察点

  1. 概念清晰度:面试官想知道你是不是真懂这三者的区别,而不是把它们当成同一个东西混着讲。很多人一张嘴就把 “分库分表” 说成一个动作,这就露馅了。
  2. 场景判断力:什么时候单库扛得住、什么时候要分表、什么时候必须分库,这种判断能力比会用中间件更值钱。
  3. 架构演进意识:分库分表从来不是上来就干的,而是单库优化、读写分离、缓存这些都做完之后,仍然扛不住才考虑的方案。能不能讲清楚这个演进过程,体现的是真实的架构经验。

核心答案

先把三个概念一刀切清楚:

概念 做的事情 解决的问题 物理层面
分表 把一张大表拆成多张表 单表数据量过大(查询慢、DDL 卡) 同一个数据库实例内
分库 把一个库拆成多个库 单库连接数、QPS、磁盘 IO 扛不住 多个数据库实例
分库分表 既拆库又拆表 数据量大 + 并发高,单库单表双双见顶 多实例 + 多表

一句话总结:分表解决 “数据多”,分库解决 “压力大”,分库分表解决 “又多又压”。

深度解析

一、为什么会出现 “数据多” 和 “压力大”

先看一个典型的 MySQL 性能瓶颈演进过程,这块很多文章一笔带过,但实际踩过坑的人都知道:

  • 单表数据量超过 500 万行 / 2GB:阿里《Java 开发手册》官方建议的 “推荐分库分表” 阈值,B+ 树层级开始增加,查询开始变慢
  • 单表数据量超过 2000 万行:这是行业广泛流传的 “性能拐点”,再优化收益也有限
  • 单库 TPS / QPS 上来:连接池打满、锁竞争加剧、磁盘 IO 饱和
  • 单库数据文件过大:备份、恢复、DDL 都变得极慢

单库单表的性能瓶颈

上面这条演进路线是面试加分项,一定要记住:分库分表是 “前面招都用完了还扛不住” 才上的最后招。我面试时常反问一句 “为什么不用读写分离和索引优化解决?”,能答上来的人不多。

分库分表前的优化路径

二、分表的两种姿势:垂直 vs 水平

垂直分表:把一张表的列拆开,按 “热冷字段” 或 “长短字段” 拆分。

比如 user 表有 30 个字段,把常用的 id、name、avatar 放主表,把不常用的 bio、address、extend_json 拆到扩展表。好处是热数据行变小,单页能装下更多行,缓冲池命中率提升。

水平分表:把一张表按行拆开,按某个分片键(如 user_id)路由到多张子表。

比如 order_0、order_1、order_2…order_15,16 张表存同一个逻辑 order 的数据。这是日常说的 “分表” 主流形态。

垂直分表和水平分表

上面两组拆法的核心差异:垂直分表减少的是 “列宽”,水平分表减少的是 “行数”。垂直分表治 “字段太杂”,水平分表治 “数据太多”。

垂直分表与水平分表对比

三、分库的两种姿势:垂直 vs 水平

跟分表一样,分库也分垂直和水平:

2. 分库分表后会带来哪些问题?

面试考察点

  1. 实战经验:分库分表不是课本上的理论,是真正在业务里趟过坑的人才能讲透。面试官想看的,是你有没有真的做过、踩过哪些坑。
  2. 全局思维:考察你能不能意识到 “分库分表不是银弹”,解决问题同时会引入新问题,有没有 “代价意识”。
  3. 技术广度:分布式事务、分布式 ID、跨库查询、数据迁移……随便拎一个出来都能聊半小时。
  4. 架构权衡能力:知道什么时候该分、什么时候不该分,分了之后怎么取舍。

核心答案

分库分表就是在 “用空间换时间、用分布式换吞吐”,代价是引入了一堆原本单机不存在的问题。核心痛点拢共就 6 类:

问题类别 核心痛点 主流解法
跨库 Join 关联查询跨多个分片,性能骤降 绑定表、广播表、业务层组装
分页排序 全局分页需要合并多分片数据 禁止深分页、ES 辅助、游标查询
分布式事务 跨库事务一致性难保证 Seata(AT/TCC/XA/Saga)、最终一致性
全局唯一 ID 自增主键失效 雪花算法、号段模式(Leaf)、UUID
数据迁移与扩容 加库加表要重新分片 一致性 Hash、双倍扩容、双写迁移
跨库聚合统计 count/sum/group by 跨库难 异步汇总表、ES、离线统计

下面挨个展开讲。

深度解析

一、跨库 Join 查询问题

原本单库一个 SELECT ... JOIN 就能搞定的事,拆库之后,订单表在 A 库,订单明细表在 B 库,Join 就废了。

跨库 Join 查询

跨库 Join 与绑定表

  • 绑定表(Binding Table):主子表用同一个分片键,比如 t_order 和 t_order_item 都按 order_id 分片,关联数据天然落在同一个库,直接本地 Join 即可。ShardingSphere 支持配置绑定表关系。
  • 广播表(Broadcast Table):字典表、配置表这种数据量小、变更少、又要频繁关联的表,直接复制到所有分片,每个库都能本地 Join。
  • Federation 引擎:ShardingSphere 5.x 引入的跨库查询引擎,支持未配置绑定表的跨库 Join,但性能不如本地 Join。
  • 业务层组装:最朴素的方案,先查主表,拿到关联字段后批量查子表,在内存里 Join。实现简单,但数据一致性得自己兜底。
二、分页排序问题

单库分页 LIMIT 10000, 10 简单粗暴,分库分表之后就头大了。

比如分了 4 个库,要查全局第 10000 页的 10 条数据,你怎么搞?每个库都得查前 10010 条,再把 4 份结果在内存里合并、排序、取第 10000~10010 条。查出来的数据量是 N 倍,内存和数据库压力都很大。

分库分表深分页

实际生产里的解法:

3. 什么是数据倾斜,会带来哪些问题?如何解决?

面试考察点

  1. 基础概念掌握度:面试官想确认你是否真的理解分区 / 分组操作在分布式计算中的本质——shuffle 之后数据如何重新分布,而不只是会背一句“数据分布不均”。
  2. 问题诊断能力:能否从症状(任务卡在 99%、某个 task OOM、整体执行时间被拖长)反推出是数据倾斜,而不是网络、GC 或代码 bug。说白了,看你有没有线上排障经验。
  3. 解决方案的深度与广度:是否能根据不同场景(group by、join、count(distinct))给出对应的解法,而不是只会一句“加分区数”。这个是拉开差距的关键。

核心答案

数据倾斜:在分布式并行计算中,数据按某个 key 分区或分组后,绝大部分数据集中到了少数几个 task / 节点上,导致负载严重不均衡的现象。

核心危害一句话:木桶效应——整个任务的执行时间被最慢的那个 task 拖死,其他节点干等着。

解决思路也一句话:让分布不均的 key 重新打散,或者绕开 shuffle。

场景 典型症状 首选解法
group by 倾斜 某 reduce task 数据量爆炸 两阶段聚合(加盐 + 局部聚合 + 全局聚合)
大表 join 小表 join 阶段卡死 Map Join(广播小表)
大表 join 大表 reduce 端 join 拖死 扩容 + 随机前缀打散
空值 / 异常值集中 某 key 是 null 占比 80% 提前过滤或单独处理
框架级兜底 不想改代码 Spark AQE / Hive 倾斜开关

深度解析

一、为什么会发生数据倾斜

分布式计算里只要触发 shuffle(group by、join、distinct、orderBy 等都会触发),数据就要按 key 重新分发到下游。分发规则一般是 hash(key) % N。

问题就出在 key 的分布上:如果某个 key 的数据量远超其他 key(比如一个电商平台里某头部主播的订单数据占全部订单的 30%),那么这个 key 所在的分区的数据量就是其他分区的几十倍甚至上百倍。

下面这张图对比了正常分布和倾斜分布:

正常分布与数据倾斜对比

  • 正常分布:100G 数据大致均匀打散到 4 个分区,每个分区处理 25G,所有 task 几乎同时完成,资源利用率高
  • 倾斜分布:某个热点 key 占了 95G 数据,几乎全部涌进同一个分区,这一个 task 处理 95G,其他三个 task 早早结束干等着

关键认知:分布式计算的总耗时 ≈ 最慢 task 的耗时,不是平均耗时。所以倾斜的代价是线性放大的。

数据倾斜的木桶效应

二、数据倾斜带来的具体问题

这块很多人只会说“慢”,其实问题有 4 个层次,面试时分得越清楚越显深度:

4. 分区和分表有什么区别?

面试考察点

  1. 概念清晰度:面试官想知道你是否能把 “分区” 和 “分表” 这两个看起来很像的概念区分开,很多人面试时把这俩混为一谈,一开口就跪。
  2. 实践深度:考察你有没有真正做过大表优化,是否知道每种方案的适用场景、成本和坑。
  3. 架构思维:分区属于单机优化,分表属于分布式架构。面试官想看你能否根据数据量和访问量选择合适的方案。

核心答案

先说结论:分区是 “一张表” 的事,分表是 “多张表” 的事。

维度 分区(Partition) 分表(Sharding)
逻辑层级 逻辑上还是一张表 逻辑上也是多张表
实现层级 数据库引擎内部(MySQL DDL) 应用层 / 中间件
对应用是否透明 透明,业务无感 不透明,需路由
是否跨实例 不能,只在单库内 可以,跨库跨实例
实现难度 简单,一条 DDL 复杂,路由/聚合/事务
突破的瓶颈 单表数据量大 单库数据量/连接/IO
MySQL 原生支持 ✅ 5.1+ 原生 ❌ 需要中间件

深度解析

一、分区(Partition):MySQL 自带的单表切分

分区是 MySQL 引擎层提供的功能,一张表在逻辑上还是一张表,但物理上把数据拆到了不同的文件里。

MySQL 分区表

MySQL 分区表

分区常见的几种类型:

  • RANGE 分区:按范围切,比如按年份、按 ID 范围
  • LIST 分区:按离散值切,比如按地区、按类型
  • HASH 分区:对某列做 hash 取模
  • KEY 分区:MySQL 内置 hash,比 HASH 更通用

举个 RANGE 分区的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE orders (
id BIGINT,
order_no VARCHAR(32),
create_time DATETIME,
amount DECIMAL(10, 2),
PRIMARY KEY (id, create_time)
)
PARTITION BY RANGE (YEAR(create_time)) (
PARTITION p2020 VALUES LESS THAN (2021),
PARTITION p2021 VALUES LESS THAN (2022),
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p_max VALUES LESS THAN MAXVALUE
);

对应用层来说完全无感,SELECT * FROM orders WHERE create_time > '2022-01-01' 这种 SQL,MySQL 内部会做 “分区裁剪”(Partition Pruning),只扫描 p2022 和 p_max 两个分区,其他分区直接跳过。

二、分表(Sharding):把一张大表拆成多张表

分表是真正的 “拆”,物理上就是多张表。分表又分两种:


二十三、高并发面试题

共 8 题。

题目清单

  1. 高并发场景中,乐观锁和悲观锁哪个更适合?
  2. 漏桶和令牌桶有啥区别?
  3. 什么是熔断?
  4. 什么是预热?它有什么作用?
  5. 什么是服务降级?
  6. 什么是限流?常见的限流算法有哪些?
  7. 什么是自适应限流?
  8. 单机限流和集群限流的区别是什么?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 高并发场景中,乐观锁和悲观锁哪个更适合?

面试考察点

  1. 概念辨析:面试官不仅仅是想知道你知不知道这两种锁的名字,更是想确认你能不能讲清楚它们的底层思想差异——一个 “先锁后操作”,一个 “先操作后验证”。
  2. 场景选型能力:核心中的核心。题干里的 “高并发” 是个伪命题,高并发读和高并发写完全是两码事,写冲突概率高和低又是两码事。能不能拆开讲,决定了这道题的分数档。
  3. 实践深度:是否熟悉 CAS、版本号、ABA、SELECT ... FOR UPDATE、synchronized 这些具体落地手段,有没有踩过坑。
  4. 工程化思维:生产环境很少单一锁方案解决一切,能不能给出 “悲观锁 + 拆分”、”Redis 预扣 + 数据库兜底” 这种混合方案,是区分高级和资深的分水岭。

核心答案

没有绝对谁更适合,关键看两个维度:写冲突概率 + 读写比例。

场景特征 推荐策略 典型业务
读多写少,写冲突概率低 乐观锁 配置更新、订单状态流转
写多,冲突概率高 悲观锁 秒杀扣库存、抢红包
超高并发写,单行热点 悲观锁 + 拆分(分桶/分段) 爆款秒杀
读多写极少 无锁 + 缓存 商品详情页

记住一句话:写少冲撞低用乐观,写多冲撞高用悲观,热点写还得靠拆分。

高并发乐观锁悲观锁选型

深度解析

一、两种锁的本质区别

先把思想讲清楚,不然后面的选型全是空中楼阁。

悲观锁:操作前先锁资源,独占到底。它假定 “一定会有人跟我抢”,所以干脆把门关上,谁也别想进来。

乐观锁:操作时不加锁,提交的时候再验证 “这段期间有没有人动过我的数据”。它假定 “大家一般不会冲突”,先干活,干完再说。

两者的流程对比:

悲观锁和乐观锁流程对比

把两种锁的执行路径并排放了。看左边的悲观锁,整个流程被一把锁从头包到尾,期间其他线程只能干等;右边的乐观锁不加锁,但在最后一步做版本校验,没冲突就提交,有冲突就重试或者直接返回失败。

关键差异就两点:

  • **悲观锁的代价是 “等待”**:其他线程被阻塞,上下文切换、锁竞争开销大
  • **乐观锁的代价是 “重试”**:冲突一多,CPU 全花在重试和自旋上

悲观锁等待与乐观锁重试

二、”高并发” 要先拆开看

很多人栽在这一步,觉得 “高并发” 就是一道题,其实它至少要拆成两个维度。

维度一:读写比例

  • 高并发读、低并发写:乐观锁完胜,因为读不阻塞、不加锁、不互相影响
  • 高并发写:要看维度二

维度二:写冲突概率

这是真正决定锁选型的核心。同样是 “高并发写”:

  • 100 个请求同时改同一行库存 → 冲突概率接近 100%,乐观锁几乎每次都要重试,反而比悲观锁还慢
  • 100 个请求改 100 个不同的账户 → 冲突概率极低,乐观锁的代价就微乎其微

所以选型的本质是:冲突高 → 悲观锁(一次锁住,避免无效重试);冲突低 → 乐观锁(避免锁开销)。

高并发读写比例和冲突概率

三、乐观锁的两种实现

2. 漏桶和令牌桶有啥区别?

面试考察点

  1. 限流基础概念:面试官不仅仅是想知道你有没有听过这两个算法,更想看你是否理解高并发场景下为什么要限流,以及两大经典算法各自的定位
  2. 算法原理理解:能否准确说出漏桶和令牌桶的工作机制,而不是停留在 “知道名字” 的层面
  3. 生产选型能力:能不能根据业务特点(突发流量 vs 平滑流量)选对算法,有没有实际落地经验

核心答案

漏桶和令牌桶都是限流算法,但思路完全不一样。

漏桶:请求像水一样倒进桶里,桶底有个固定大小的洞,水以恒定速率漏出。不管你倒水多猛,漏出去的速度永远不变——强制平滑流量,不允许突发。

令牌桶:桶里以固定速率往里放令牌,请求来了先从桶里拿一个令牌,拿得到就处理,拿不到就拒绝或排队。桶有上限,攒满了就不再放——允许一定程度的突发流量。

最核心的区别一句话:漏桶出去的是匀速的,令牌桶进来的是匀速的(但出去可以快)。

对比项 漏桶 令牌桶
核心思想 出口恒速 入口匀速放令牌
突发流量 不允许,强制平滑 允许,攒够令牌就能瞬时通过
流量整形 强 弱
实现复杂度 简单 稍复杂
适用场景 保护下游脆弱系统 API 网关、开放平台
常见实现 Nginx limit_req Guava RateLimiter、Spring Cloud Gateway

漏桶和令牌桶区别

深度解析

一、漏桶算法(Leaky Bucket)

漏桶你可以想象成一个真实的水桶,桶底有个小孔漏水。

漏桶算法流程

  • 入桶阶段:请求以任意速率到达,只要桶还没满就放进来,满了直接拒绝
  • 出桶阶段:桶里的请求以固定速率被取出,送往下游处理
  • 关键点:不管上游来得多猛,下游永远以匀速接收

漏桶最大的价值就是 强制整形——把崎岖不平的流量,强行削成一条平滑的直线。对下游非常友好。

但缺点也很明显:完全不能处理突发。哪怕下游其实有能力处理一波瞬时高并发,漏桶也会无情地把它削平。就算系统资源空闲,也只能干等着。

漏桶算法流量整形

二、令牌桶算法(Token Bucket)

令牌桶换了个思路:不是控制水流出去,而是控制 “通行证” 的发放。

令牌桶算法流程

  • 令牌生成:以固定速率往桶里放令牌,桶有容量上限,满了就丢弃多余的
  • 请求处理:每个请求来了,必须从桶里取走一个令牌才能通过,取不到就被拒绝
  • 突发能力:如果一段时间没请求,桶里能攒满令牌;这时候来一波突发流量,能瞬间消耗掉所有令牌,相当于瞬时通过大量请求

令牌桶的关键在于:长期平均速率恒定,但短期允许突破。这个特性很贴合真实业务——大多数业务就是 “平时低峰 + 偶尔突发” 这个节奏。

令牌桶算法允许突发

三、用代码直观感受差异

用 Guava 的 RateLimiter 来体验一下令牌桶:

3. 什么是熔断?

面试考察点

  1. 稳定性意识:面试官想知道的不只是你会不会写熔断代码,更看重你脑子里有没有“系统随时会挂”这根弦。分布式环境下故障是常态,能不能意识到这点,是另一个层面的问题。
  2. 雪崩效应的理解:知不知道一个下游服务挂了,为什么会拖垮整条链路?这是熔断存在的根本原因。
  3. 原理深度:熔断器的状态机(Closed → Open → Half-Open)是不是真的清楚,还是只会调个注解就完事。
  4. 框架落地能力:Hystrix、Sentinel、Resilience4j 这几个主流选手各自的定位和差异能不能说清楚。

核心答案

一句话:熔断(Circuit Breaker)是一种自我保护机制。下游服务出故障或响应过慢,达到某个阈值后,熔断器主动 “跳闸”,在一段时间内直接拒绝或降级对该服务的调用。否则故障会顺着调用链一路扩散,把整个系统拖垮。

你可以把它想象成家里的保险丝:电流一过载,保险丝 “啪” 一下断了,保护电器不被烧坏。等电工来修好,再手动合闸恢复供电。熔断器干的也是这事儿,只不过它自己会 “试合闸”。

熔断 vs 降级 vs 限流,这三个概念很多人容易搞混,先一张表理清楚:

机制 干啥的 触发条件 类比
限流 控制请求量,防止系统被流量压垮 QPS / 并发数超阈值 景区检票口限流
熔断 切断对故障服务的调用,防止雪崩 失败率 / 慢调用比例超阈值 保险丝跳闸
降级 主流程走不通时走兜底逻辑 熔断触发、超时、异常等 飞机备降

一句话总结:限流防自己被压垮,熔断防被下游拖垮,降级是兜底手段。熔断触发后通常会走降级逻辑。

高并发熔断机制

限流熔断降级区别

深度解析

一、为什么需要熔断——雪崩效应

熔断存在的意义,全在 “雪崩” 这两个字里。我画个图你就明白了:

熔断雪崩效应流程

假设通知服务挂了,每次响应从 50ms 飙到 30 秒。这时候支付服务还在傻乎乎地调它,请求一个接一个发出去,每个都要等 30 秒才超时。结果就是:

  • 支付服务的线程池被占满:所有线程都阻塞在等通知服务响应
  • 支付服务也跟着不可用:新来的请求拿不到线程,全部排队
  • 订单服务调不到支付服务:自己线程池也被占满
  • 网关层请求堆积:连锁反应一路向上
  • 整条链路雪崩:一个边缘小服务,搞挂了整个系统

这个过程就像雪崩:一小块雪松动,带动一大片,最后整个山坡都下来。没有熔断,故障会在调用链上指数级扩散。

熔断就是给每条调用链装上 “保险丝”。发现下游不对劲(失败率高、响应慢)就立刻断开,快速失败,把线程释放出来给其他正常请求用。一边牺牲掉对故障服务的调用,一边保住系统其他部分的可用性。

服务雪崩效应

二、熔断器的三大状态

熔断的核心是一个状态机,三个状态来回切换。这是这道题的真正考点,得背下来:

熔断器状态机


二十四、高可用面试题

共 2 题。

题目清单

  1. 什么是异地多活?
  2. 什么是冷备、热备,暖备?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 什么是异地多活?

面试考察点

  1. 概念理解:面试官想看你是否真正理解 “异地” 和 “多活” 这两个词,很多人会把它和 “主备”、“冷备” 混为一谈。真正的多活,多个机房都在对外提供服务,不是备胎。
  2. 架构权衡意识:异地多活不是银弹,它牺牲了一致性来换可用性。面试官想听你聊聊 CAP、网络延迟、数据同步这些痛点。
  3. 落地经验:纯背概念和真做过的人,一开口就分得出来。比如单元化路由、DTS 数据同步、全局 ID 这些细节,是真踩过坑才说得清的。

核心答案

异地多活:在不同地理位置的多个机房同时对外提供服务,任何一个机房挂了,其他机房能快速接管,业务基本不中断。

异地多活故障自动切换

注意三个关键词:

  • 异地:物理距离上拉开(同城双活、两地三中心、跨省多活都算),目的是防火灾、地震、停电、整城断网这类机房级 / 城市级故障
  • 多活:每个机房都在干活,不是冷备、热备那种 “备胎”,挂了再切。多活是同时写、同时读
  • 业务可用:允许少量数据丢失,只要把故障影响控制在业务可接受范围内

跟几个容易混淆的概念对比一下:

方案 机房角色 切换速度 建设成本 适用场景
冷备 主写,备机不跑业务 几小时 低 小系统、非核心业务
热备 主写,备机同步数据 分钟级 中 一般业务
同城双活 双写双读 秒级 中高 核心业务、容灾机房级故障
异地多活 多写多读 秒级 高 超大流量、城市级容灾

深度解析

一、为什么要搞异地多活

先想一个问题:单机房挂了会怎样?

举个例子,2021 年某云厂商机房光缆被挖断,大量公司业务中断几个小时。如果你的业务只在单机房跑,机房一挂,DB、缓存、应用全挂,这是机房级单点。

那同城双机房够不够?不够。火灾、地震、整城断网这种黑天鹅事件,同城两个机房一起挂。所以才有 “异地”,把机房拉开到不同城市,物理上隔离故障域。

但又不能纯异步备份(冷备/热备),因为切换太慢,业务可能要停几分钟甚至几小时。所以才有 “多活”,平时大家都在干活,挂一个剩下的立刻顶上。

二、异地多活的核心架构

异地多活核心架构

上面是一个典型的异地多活架构,分几个关键层:

  • 流量入口层:智能 DNS(或 HTTP DNS)根据用户 ID、地理位置、机房健康度,把请求路由到对应机房。这一层是异地多活的 “大脑”,决定了用户去哪儿
  • 应用层:每个机房都有完整的应用集群,能独立对外提供服务
  • 数据层:每个机房都有自己的 DB 和缓存,机房之间通过数据同步通道(DTS、Canal、Otter)异步同步数据
  • 路由策略:核心是单元化,按业务维度(用户 ID、订单 ID)把数据切分到不同机房,避免跨机房双写冲突
三、最大的痛点:数据一致性

异地多活最难的其实不是搭几个机房,真正难的是数据同步。

为什么难?因为跨地域网络延迟很高:

2. 什么是冷备、热备,暖备?

面试考察点

  1. 概念清晰度:面试官想知道你能不能把冷备、暖备、热备的区别讲清楚,而不是含糊其辞一通混。这三个词很多人只会背定义,真问起来又分不清。
  2. 场景理解:光背概念没用,更关键的是——什么场景该用哪种?面试官想看你有没有 “花小钱办大事” 的意识,是不是真做过选型,别上来就堆热备双活。
  3. 指标意识:高可用的核心指标 RTO(恢复时间目标)和 RPO(恢复点目标),这俩词能不能跟三种备份方式对应起来。提一嘴这俩指标,加分。

核心答案

先上一张对比表,三者的核心差异就在 “备机状态、数据同步方式、恢复速度、成本” 这几条上:

备份方式 备机状态 数据同步 恢复速度(RTO) 数据丢失(RPO) 成本
冷备 关机/未运行 不同步,靠定期备份文件 几十分钟~几小时 可能丢几小时数据 低
暖备 运行中,待命 异步/定期同步 几秒~几分钟 秒级~分钟级 中
热备 运行中,实时同步 实时同步,可自动切换 接近 0(自动切换) 几乎不丢 高

一句话总结:冷备 = 备机睡觉、热备 = 备机盯着、暖备 = 备机半睡半醒。

冷备暖备热备对比

深度解析

一、一张图看懂三者差异

冷备暖备热备切换流程

  • 冷备:故障 → 启动备机 → 恢复数据 → 切流量,整条链路最长,往往要几十分钟甚至更久。备机平时根本不跑业务。
  • 暖备:备机一直开着,但数据是异步/定期同步的。故障发生时不用从零启动,直接把流量切过去,但可能有少量数据丢失或需要人工介入。
  • 热备:备机和主机数据实时同步(通常用 binlog / WAL / Raft 这类机制),故障时由心跳检测 + 自动 failover 秒级切换,用户几乎无感。
二、结合实际,三者分别长什么样

冷备:典型场景就是数据库每天夜里备份出来的 dump 文件,丢到另一台机器或对象存储里放着。备机平时可能压根没开,等出事了再拉起来恢复。

我见过不少小公司内部系统就这么玩——一周一次全量备份,丢到 OSS 上,挂了就用备份恢复。RTO 几小时也能接受,毕竟不是核心链路。

暖备:MySQL 的异步主从、Redis 的主从复制,其实都算暖备。备机是活的,能接受读流量(读写分离就是这么来的),但故障切换往往要半人工或靠 MHA / Sentinel 这类工具辅助。

说个坑:很多人以为有了主从就万事大吉,其实异步复制在主机突然宕机的瞬间,未同步的那部分 binlog 就丢了。这就是 RPO 不为 0 的根源。

热备:典型代表是 Redis Sentinel 的自动故障转移、MySQL MGR(Group Replication)的强一致集群、Paxos/Raft 共识算法保障的多副本系统(比如 etcd、TiKV)。数据在写主的同时立刻同步到备,主机一挂,秒级选出新主。


二十五、Elasticsearch 面试题

共 9 题。

题目清单

  1. 为什么要使用 ElasticSearch?和传统关系数据库(如 MySQL)有什么不同?
  2. ElasticSearch 为什么快?
  3. ES 支持哪些数据类型,和 MySQL 之间的映射关系是怎么样的?
  4. 倒排索引是什么?
  5. ES 不支持 decimal,如何避免丢失精度?
  6. Elasticsearch 支持事务吗?为什么?
  7. 什么是 Elasticsearch 的深度分页问题?如何解决?
  8. 如何优化 ElasticSearch 搜索性能?
  9. 如何保证 ES 和数据库的数据一致性?

高频题详解

每道题按「考察点 → 核心答案 → 深度解析」组织:先看面试官想考什么,再看怎么答,最后补原理。受单篇篇幅限制,深度解析只保留主干与关键结论,代码示例做了压缩。

1. 为什么要使用 ElasticSearch?和传统关系数据库(如 MySQL)有什么不同?

面试考察点

  1. 技术选型意识:面试官想看的其实是你有没有想清楚——什么场景该用 ES,什么场景 MySQL 就绰绰有余。盲目上 ES 也是要扣分的。
  2. 原理理解深度:倒排索引 vs B+Tree 索引,这是这道题的灵魂。能讲清楚这两个数据结构的差异,才算真正理解“为什么快”。
  3. 架构实践经验:实际项目里 MySQL 和 ES 怎么分工、数据怎么同步,这是区分“背过八股”和“真用过”的分水岭。

核心答案

先说结论:MySQL 是“存”的专家,ES 是“搜”的专家。两者不是谁取代谁,更像一对搭档,各管一滩。

为什么要用 ES?因为传统关系数据库在“搜”这件事上有硬伤:

痛点场景 MySQL 的表现 ES 的表现
模糊搜索 LIKE '%手机%' 前置 % 导致索引失效,全表扫描 倒排索引直接命中,毫秒级返回
全文检索 + 分词 不支持分词,只能整串匹配 内置分词器,搜“手机”能命中“智能手机”
多条件组合查询 多字段 LIKE + OR,性能崩塌 结构化 + 全文混合查询,统一 DSL
相关度排序 只能按字段排序,没有“相关度”概念 BM25 算法打分(ES 5.0 起默认,取代 TF-IDF),最相关的排最前
聚合统计分析(类似 GROUP BY) 数据量大时分组聚合很慢 列式存储 + 预聚合,亿级数据秒出

说白了:数据量上了千万、搜索条件一复杂,MySQL 就开始喘,ES 就是为这种场景准备的。

ES MySQL 区别

深度解析

一、MySQL 的 B+Tree,为什么“搜不动”

MySQL(InnoDB 引擎)的索引底层是 B+Tree。它本质上是把索引列的值排好序,查询时从根节点往下走,时间复杂度稳定在 O(log n),等值查询和范围查询都非常快。

但它的前提是:能走上索引。来看这个 SQL:

1
2
-- 想搜标题里带"手机"的商品
SELECT * FROM product WHERE title LIKE '%手机%';

% 放在前面,意味着开头可以是任意字符,B+Tree 的有序性完全用不上,优化器只能放弃索引,走全表扫描,一行一行扫过去,挨个做字符串匹配。

这里补个容易被追问的细节:如果查询的列刚好都在某个二级索引里(覆盖索引),MySQL 有时会选择全扫描那棵更小的二级索引树,而不去扫聚簇索引,代价小一些。但本质上还是“整棵树挨个摸一遍”,数据量一大照样慢,没有质的区别。

100 万数据可能还能忍,1000 万、上亿呢?用户在搜索框敲下回车,等来的是超时。

1
2
-- 这种能走索引,因为前缀固定
SELECT * FROM product WHERE title LIKE '手机%';

但真实业务里,用户搜的词在标题的哪个位置都有可能,后置匹配根本不解决问题。

LIKE 索引失效

二、ES 的倒排索引,为什么“搜得快”

2. ElasticSearch 为什么快?

面试考察点

  1. 原理深度:光答 “倒排索引” 这四个字不够,面试官要听的是你能不能讲清它内部的查询结构(Term Index、Term Dictionary、Posting List),这一条直接区分 “背答案的” 和 “真懂的”。
  2. 对比思维:考察你能不能把 ES 和 MySQL 的 B+ 树做对比,说清楚各自擅长什么场景。能做对比的人,通常真用过这两个东西。
  3. 架构视野:除了单机数据结构,面试官还想看你能否从分布式(分片并行)、近实时(NRT)、缓存这些架构层面来分析性能。答到这层,冲高级岗就有底气了。

核心答案

先给结论:ES 快是一堆优化叠出来的,没有单点魔法,核心可以拆成三层看:

层面 关键设计 一句话解释
数据结构层 倒排索引 从 “关键词” 直接定位 “文档”,跳过全表扫描
数据结构层 FST + Term Index 词典的 “目录页” 常驻内存,磁盘最少寻址一次
数据结构层 FOR 压缩 + 跳表 + Roaring 位图 Posting List 压缩存储,多条件求交集飞快
存储层 Segment 不可变 无锁读取、友好压缩、吃满 Page Cache
存储层 Doc Values 列式存储 排序聚合不用回源解析原文
架构层 分片并行查询 一份查询拆到 N 个节点同时算
架构层 NRT 近实时写入 1 秒可见性,换来写入高吞吐

面试时建议按 “数据结构 → 存储 → 架构” 这个顺序答,层次感一下就出来了。

ES 为什么快

深度解析

一、倒排索引:一切的起点

先搞清楚正排和倒排的区别。MySQL 里存的是 “文档 → 内容”,这就是正排。你想搜包含 “关注” 两个字的文章,数据库只能一条条翻,like '%关注%' 直接全表扫描,数据量一大就是灾难。

倒排索引反过来,存的是 “关键词 → 文档列表”:

ES 倒排索引

  • 正排索引:从文档找内容容易,但从内容找文档,只能逐条匹配
  • 倒排索引:写入时先分词(“关注犬小哈” 会拆成 “关注”、“犬小哈” 两个 Term),然后建立 “Term → 文档 ID 列表” 的映射
  • 查询时:搜 “犬小哈” 直接拿到 Posting List [1, 2],O(1) 级别定位,根本不用扫描原文

一句话:写入时多干活(分词、建索引),查询时少干活。搜索引擎 20 年前的核心思想,到现在依然能打。

ES 倒排索引

但光有倒排索引还不够。假设 Term 有上亿个(英文单词、中文词、各种数字组合),怎么快速找到目标 Term 本身就是个问题。总不能每个查询都在磁盘上二分查找几万次吧?

二、Term Index + Term Dictionary:字典的 “目录页”

Lucene 把词典查询拆成了三层结构,这是我认为整道题最出彩的部分:

ES 词典查询

这三层的文字讲解:

3. ES 支持哪些数据类型,和 MySQL 之间的映射关系是怎么样的?

面试考察点

  1. 基础掌握度:数据类型是 Mapping 的地基。面试官想知道你是真的建过索引、写过 DSL,还是只在项目里 “摸过” ES。
  2. 选型能力:text 和 keyword 选错、金额用了 double,这些在线上都是实打实的事故。类型选型直接暴露你的实践深度。
  3. 知识迁移能力:业务里最常见的场景就是 “MySQL 的数据同步到 ES 做搜索”。能不能把 MySQL 的概念、字段类型准确翻译成 ES 的,考察的是你对两套存储体系的理解,死记硬背应付不了追问。

核心答案

ES 的数据类型可以分成五大类:

分类 常用类型 一句话说明
核心类型 text、keyword 字符串二兄弟,一个分词一个不分
数字类型 long、integer、short、byte、double、float、half_float、scaled_float 整数 4 档,浮点 5 档
时间类型 date 既能存时间戳也能存格式化字符串
布尔类型 boolean 就是 true / false
复杂类型 object、nested、join、geo_point、ip、dense_vector 等 对象、嵌套、父子、地理、向量

和 MySQL 的概念映射一张表说清楚:

MySQL ES 备注
Database Index ES 7.0 之后没有 Type 的概念了
Table Type(已废弃)→ 现在直接对应 Index 老面试题里的 “Type” 是历史产物
Row Document 一行数据就是一个 JSON 文档
Column Field 文档里的一个字段
Schema Mapping 定义字段名和类型
SQL Query DSL 查询语言

ES 数据类型

深度解析

一、先搞懂 ES 独有的 text 和 keyword

这是和 MySQL 差异最大的地方,也是面试的重头戏。

MySQL 的 VARCHAR 一个字段既 WHERE name = '手机' 精确匹配,又 LIKE '%手机%' 模糊查询,一个字段两副面孔。到了 ES 这里,拆成了两个类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT /product
{
"mappings": {
"properties": {
"productName": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}

上面这段是生产环境最经典的写法,拆开看:


写在最后

刷题这件事有个反直觉的地方:决定通过率的不是「看过多少道」,而是「能不能用自己的话讲清楚」。

所以最好的复习方式不是把答案读一遍,而是遮住答案,对着题目口述 60 秒,再回头对一遍要点——对不上的地方,才是真正要补的地方。

这份手册的价值在于「结构化」:它把散落的问题按方向归了类,让你知道自己缺的是哪一块。至于每一块的深度,还得靠自己去补源码、补实践。

祝你面试顺利。


说明:本文题目与答案整理自公开的 Java 面试资料,仅供个人复习参考;技术细节请以官方文档与源码为准。