Java全骨架(基础+大厂面经)
2026-05-23 15:15:49 0 举报AI智能生成
6 年 Java 后端实战沉淀,14 大模块 240+ 知识点全骨架。 【最大差异】每条面经都附真实题号 + 公司轮次([字节 R1] #4748、 [蚂蚁 R4] #3428),可溯源,非 ChatGPT 编造。 【内容覆盖】MySQL 26 / Redis 20 / Kafka 20 / JVM 27 / 并发 30 / Java 基础 28 / Spring 14 / 网络 13 / OS 11 / ES 6 / 微服务 10 / 分布式 13 / 系统设计 13 / DDD 13。 【三段式骨架】每个 KP:① 骨架 ≤ 7 条(最小可面试单元) ② 易混淆专项(拦面试官陷阱)③ 面经快问 5+ 道,覆盖 3 家以上公司。 【ROI 标注】每条都分 ★ 必背 / ☆ 加分 / 无标背景,告诉你 "考试时间有限,先记哪条",不是流水账列表。 适合:3-6 年后端跳槽、社招冲大厂、定向准备阿里 / 字节 / 美团 / 拼多多 / 蚂蚁 / 滴滴 / 腾讯 / 网易 / B 站 / 携程 / 京东。
后端面试
Java 八股
大厂面经
社招准备
复习导图
模板推荐
作者其他创作
大纲/内容
MYSQL
S1 索引的艺术
b-plus-tree-principle
🎯 骨架
多叉平衡树:非叶子节点只存 key + 子页指针不存数据,叶子节点存完整数据,叶子间双向链表
页(16KB)为单位 IO:一个 Page 可存 ~1000 条索引项,3 层就能撑 ~亿级数据,磁盘 IO 至多 3 次
范围查询天然友好:叶子双向链表 → 找到起点后沿链表扫,无需回退根节点
vs B 树:B 树非叶子也存数据,扇出小,相同数据需更多层级 → 更多 IO;范围查询要中序遍历,慢
vs Hash 索引:Hash O(1) 等值快,但不支持范围、排序、模糊;不能用于联合索引最左前缀
⚠️ 易混淆
树高 3 层指 InnoDB 实际情况下的常态,理论上 4-5 层也存在(极大表 / 大主键)
B+ 树非叶子节点不存数据,所以扇出大、树矮(H 小)
红黑树不适合磁盘索引:树太高(log₂ N),磁盘随机 IO 多,但适合内存场景(HashMap.TreeNode)
🎤 面经
[蚂蚁]R1 #6848 数据库索引结构,为什么用 B+ 树,还有其他的索引结构可以使用么?
答:B+ 树扇出大、树矮 IO 少、范围查询友好;其他可用 Hash(等值)/ LSM-Tree(写多读少 OLAP)
[小米]R3 #7231 用到了 mysql,说了下 acid 原则和索引。为什么索引用 b+ 而不是 b
答:B+ 树非叶子不存数据,单页能放更多索引项,扇出大、树矮、IO 少;范围查询走叶子链表无需中序遍历
[百度]R2 #7156 为什么用 B+ 树?和 HashMap 的红黑树的区别?
答:B+ 树面向磁盘 IO(块状读写),红黑树面向内存(节点访问),磁盘场景树高致命
[网易]R1 #3273 mysql 索引是怎么实现的?b+ 树有哪些特点?真实的数据存在哪里?
答:B+ 树存索引;非叶子做路由,叶子存完整行(聚簇)/ 主键值(二级索引);数据在叶子节点
[百度]R1 #2852 索引树是如何维护的?
答:插入时定位叶子页 → 满则页分裂;删除时合并相邻页或允许碎片;自增主键能避免分裂
clustered-vs-secondary
🎯 骨架
聚簇索引 = 主键索引,叶子节点存完整行数据,一张表只有一个
二级索引叶子节点存主键值(不是行地址),查其他字段需回表
回表 = 拿主键值去聚簇索引再查一次完整行
覆盖索引 = 查询列全在索引中,不回表,EXPLAIN 显示 Using index
自增主键 vs UUID:自增尾部插入无页分裂;UUID 随机插入频繁页分裂 + 碎片
⚠️ 易混淆
二级索引叶子存的是主键值,不是 GUID、不是行地址(MyISAM 才存地址)
主键查询不存在回表(聚簇索引直接拿完整行)
InnoDB 没有显式主键时,用第一个非空唯一索引;都没有则隐式生成 6B rowid
heap_no 是 Page 内记录的物理插入顺序编号,逻辑顺序靠 next_record 指针维护(两者不同)
🎤 面经
[百度]R2 #7174 MySQL 有哪几种索引?聚簇索引和辅助索引的区别?
答:聚簇=主键索引,叶子存完整行;辅助索引(二级索引)叶子存主键值,需回表
[Shopee]R1 #6442 通过聚簇索引和非聚簇索引的查找过程
答:聚簇直接定位叶子拿行;非聚簇先找主键值再去聚簇查行(这一步叫回表)
[拼多多]R3 #6920 InnoDB 使用什么方式实现索引?聚簇和非聚簇的区别?
答:B+ 树;聚簇叶子存行数据,非聚簇叶子存主键,需回表
[小米]R1 #3164 聚簇索引和非聚簇索引的区别?
答:InnoDB 中聚簇 = 主键索引;辅助索引(非聚簇)叶子存主键值
[蚂蚁]R1 #6790 围绕 id 和 classId 作为索引的区别,讲讲聚簇索引和其他索引的区别
答:主键 id = 聚簇索引,叶子存完整行;classId 二级索引叶子存主键,查 * 要回表
[蚂蚁]R1 #6821 聚簇索引和非聚簇索引;当前读和快照读
答:聚簇 vs 非聚簇核心是叶子存什么;当前读用排他锁读最新版,快照读读历史版本
composite-index
🎯 骨架
最左前缀原则:联合索引 (a,b,c),使用顺序必须从 a 开始才能命中索引(a / a,b / a,b,c 都能用)
范围查询截断:a=? AND b>? AND c=? → c 上的索引失效(b 是范围查询,c 在内层无序)
索引列顺序设计:
高频等值查询的列放最左
区分度高(基数大)的列优先
范围查询的列放最右
覆盖索引技巧:(a,b,c) 索引上 SELECT a,b,c 不回表
ICP 索引下推(5.6+):MySQL 把 b、c 的过滤下推到引擎层,先在索引上过滤,再回表
⚠️ 易混淆
联合索引 (a,b,c):能用 a / a,b / a,b,c;不能单独用 b / c / b,c
WHERE b=? AND a=? 也能命中(优化器会重排顺序)
范围查询是 > < BETWEEN,等值查询是 =,IN 通常被当成等值
🎤 面经
[百度]R2 #7195 联合索引 (a,b,c) 在查询时可以只用到 a 和 c 吗?
答:不能,最左前缀要求连续,跳过 b 后 c 无序,只能用 a
[京东]R2 #3069 建立了三个单列索引 a,b,c 查询 where a=? b=? c=? 索引会分别起作用吗
答:不会,优化器会选其中一个最优的或走 Index Merge;建议建联合索引 (a,b,c)
[携程]R1 #1695 联合索引创建和使用注意什么?
答:最左前缀;高频等值列放左;范围列放右;考虑覆盖索引;用 EXPLAIN 验证
[B站]R1 #2766 MySQL 索引分类,如何创建联合索引,有什么原则?
答:最左前缀匹配原则
[蚂蚁]R1 #6639 联合索引 ABC 的执行情况,特别是 A=XXX and C<XXX
答:只命中 A 上的索引,C 上的范围条件因 B 缺失而无法用索引
covering-index
🎯 骨架
定义:查询的列全部包含在索引中,不需要回表查聚簇索引
EXPLAIN 标志:Extra 列显示 Using index(不是 Using index condition,那是 ICP)
典型场景:
SELECT 主键 / 索引列 → 二级索引叶子已包含所需数据
联合索引上 SELECT a,b FROM t WHERE a=?,索引 (a,b) 直接覆盖
优化技巧:把高频访问的小字段加到联合索引末尾,省回表
代价权衡:索引变宽 → 占空间多、写入慢、单页存的索引项少(Page 利用率下降)
⚠️ 易混淆
覆盖索引 ≠ 联合索引,但联合索引常用来构造覆盖索引
SELECT * 几乎不可能用上覆盖索引(除非主键 = 唯一索引)
覆盖索引省的是回表 IO,不是索引扫描本身
🎤 面经
[百度]R2 #7205 介绍下 MySQL 的回表和覆盖索引?
答:回表 = 二级索引找到主键再去聚簇索引查行;覆盖索引 = 查询列全在索引中,跳过回表
[小米]R1 #2119 什么是覆盖索引和最左前缀原则?
答:覆盖索引:查询字段都在索引里,免回表;最左前缀:联合索引必须从最左列开始用
[携程]R1 #1695 联合索引 a b c where 后面已经 ok,select 写什么满足覆盖索引?
答:SELECT 列必须全部在 (a,b,c) 中(含主键),EXPLAIN 显示 Using index
[Shopee]R2 #765 什么是覆盖索引?什么是 Hash 索引?和 B+ 树有什么区别?
答:覆盖索引免回表;Hash 索引等值查询 O(1) 但不支持范围/排序
[小红书]R1 #5340 举个不用回表的例子(覆盖索引)
答:联合索引 (name,age),SELECT age FROM t WHERE name=? → age 已在索引中,免回表
[携程]R1 #2915 聚集索引和覆盖索引
答:聚集是表数据存储方式(叶子=完整行);覆盖是查询优化技巧(索引能满足查询字段)
index-failure-cases
🎯 骨架
左通配符:LIKE '%xxx' → 无法从左定位,全表扫
函数/运算:WHERE YEAR(created_at)=2026 / WHERE id+1=10 → 破坏索引有序性
隐式类型转换:phone 是 VARCHAR,WHERE phone=13800138000(数字)→ 字符串转数字全表扫
联合索引不满足最左前缀:跳过最左列,或范围查询后的列失效
OR 有非索引列:OR 两边只要有一边没索引,整体走全表扫
⚠️ 易混淆
!= 和 NOT IN 不是绝对失效,优化器根据数据分布判断(过滤少量数据时可能走索引)
OR 两边都有索引时可能走 Index Merge(索引合并),不一定失效
范围查询(> < BETWEEN)后面的列失效,但范围列本身还是走索引的
🎤 面经
[Shopee]R1 #2768 MySQL 索引在什么情况下会失效
答:五大场景:左通配/函数运算/隐式类型转换/最左前缀违反/OR 含非索引列
[百度]R2 #7153 索引失效的情况说说?联合索引有什么原则?
答:五大失效场景;联合索引最左前缀
[京东]R1 #5474 索引失效的场景有哪些
答:同上五大场景
[腾讯]R5 #7080 MySQL 索引介绍/聚集 vs 非聚/索引选择/索引失效/索引下推/B+ 树
答:失效五场景 + ICP 把 WHERE 下推引擎层减少回表
[滴滴]R4 #5441 InnoDB 索引数据结构/多级索引命中场景/索引失效场景
答:失效场景同上 + EXPLAIN 验证
[Shopee]R1 #1009 为什么类似 like %张三% 会使索引失效(从底层原理)?
答:左通配符无法从索引最左列定位起点,B+ 树有序结构失去意义,退化全表扫
index-pushdown
🎯 骨架
全称:Index Condition Pushdown,MySQL 5.6 引入
核心动作:把 WHERE 条件中联合索引覆盖到的列的过滤,从 Server 层下推到存储引擎层
优化效果:先在索引上过滤掉不符合条件的行,再回表 → 减少回表次数
EXPLAIN 标志:Extra 显示 Using index condition
触发条件:联合索引 + 索引中包含的列在 WHERE 中(即使不是最左前缀连续)
⚠️ 易混淆
没 ICP 之前:引擎层只用最左前缀过滤,回表后由 Server 层做剩余条件过滤
有 ICP 之后:所有索引列都能在引擎层过滤,再决定要不要回表
ICP ≠ 覆盖索引:ICP 仍要回表,只是减少了回表的行数;覆盖索引完全不回表
ICP 对 InnoDB 默认开启,可通过 optimizer_switch 关闭
🎤 面经
[腾讯]R1 #3110 项目中用过哪些 MySQL 索引优化措施?什么是 MySQL 索引下推?
答:ICP 把 WHERE 过滤下推到引擎层,先在索引上过滤再回表,减少回表次数
[腾讯]R5 #7080 索引选择/索引失效/索引下推/为什么选 B+ 树
答:ICP 优化:联合索引 (a,b,c),WHERE a=? AND c LIKE '%x%' 中 c 也能在引擎层过滤
[阿里]R1 #86 数据库索引下推的适用场景是什么?提升性能的原因?
答:适用联合索引 + 多列 WHERE;性能提升点在减少回表 IO
[阿里]R2 #28 最左匹配/索引覆盖/索引下推/索引失效/查询优化
答:ICP 弥补了"非最左前缀连续列"无法用索引的部分缺陷
S2 事务与一致性
mvcc-readview
🎯 骨架
MVCC = 多版本并发控制,读不加锁,靠版本链实现
每行数据有隐藏列:trx_id(最后修改的事务ID)+ roll_pointer(指向 undo log 旧版本)
ReadView 四要素:creator_trx_id / m_ids(活跃事务列表)/ min_trx_id / max_trx_id
可见性判断:trx_id < min → 可见;trx_id ≥ max → 不可见;中间看是否在 m_ids 里
RC 每次 SELECT 生成新 ReadView;RR 只在第一次 SELECT 生成,后续复用
⚠️ 易混淆
max_trx_id 是"下一个将分配的事务ID",不是当前最大的
RR 下快照读不会幻读(ReadView 固定),但当前读(SELECT FOR UPDATE)仍可能幻读,靠间隙锁解决
🎤 面经
[百度][R2] MVCC?版本号怎么变化的?幻读怎么解决?→ trx_id 全局递增写入行隐藏列,旧版本串 undo log 版本链;快照读靠 ReadView(新行 trx_id ≥ max 不可见),当前读靠间隙锁阻止插入
[蚂蚁][R1] 当前读与快照读区别?→ 快照读=普通 SELECT,走 MVCC 读历史版本不加锁;当前读=FOR UPDATE/DML,读最新版本加锁,转账查余额必须当前读
[小米][R3] ACID 每项怎么保证?→ A(Atomicity 原子性)=undo log 逆向回滚 / D(Durability 持久性)=redo log WAL 持久化 / I(Isolation 隔离性)=MVCC+锁 / C(Consistency 一致性)=A+I+D 的结果+约束
长事务:事务还活着,ReadView 存活,Purge 被阻塞,版本链持续增长
死事务:事务已结束,但 undo log 物理清理(Purge)未完成,History list length 仍高
两者症状相同(History list length 高),区分靠 INNODB_TRX 是否还有活跃事务
影响范围:Buffer Pool 全局共享,undo 页膨胀后整库读性能下降,不只是长事务本身
isolation-levels
🎯 骨架
四级隔离(从弱到强):
RU(Read Uncommitted):脏读 / 不可重复读 / 幻读 都有
RC(Read Committed):解决脏读,不可重复读 / 幻读 有
RR(Repeatable Read):解决脏读 + 不可重复读,幻读 有(InnoDB 用 MVCC + 间隙锁解决了大部分)
Serializable:全部解决,串行执行,性能差
三大异常区分:
脏读:读到其他事务未提交的修改
不可重复读:同一事务两次读同一行,结果不同(被其他事务修改)
幻读:同一事务两次范围查询,行数不同(被其他事务插入/删除)
InnoDB 默认 RR,互联网常降到 RC(避免间隙锁死锁)
RC vs RR 核心区别:ReadView 生成时机不同(RC 每次新建 / RR 复用第一次)
降级到 RC 前提:binlog 必须是 Row 格式(Statement 格式 RC 下主从不一致)
⚠️ 易混淆
不可重复读关注单行修改,幻读关注行数变化
RR 下快照读不幻读(MVCC),但当前读(FOR UPDATE)会幻读,靠间隙锁兜底
RC 没有间隙锁,所以没有间隙锁死锁
🎤 面经
[拼多多]R3 #6947 事务的隔离级别
答:四级:RU/RC/RR/Serializable,对应解决脏读/不可重复读/幻读/全部
[快手]R1 #2730 MySQL 事务的隔离级别;什么是幻读,MySQL 如何避免?什么是间隙锁?
答:RR + 间隙锁解决幻读;间隙锁仅 RR 下,当前读触发
[快手]R2 #6333 MySQL 事务隔离级别,有什么区别,什么是脏读幻读不可重复读?
答:三大异常对应三级,RR 解决前两个,间隙锁配合解决幻读
[小米]R3 #7219 小米商城下单提交前和提交后各隔离级别读到什么?
答:RU 读未提交脏数据 / RC 读已提交但不可重复 / RR 快照固定可重复 / Serializable 串行
[百度]R2 #7194 数据库的隔离级别,分别解决了什么问题
答:同上四级 vs 三异常
[蚂蚁]R1 #6713 MySQL 事务隔离级别,什么时候脏读,什么时候读已提交
答:脏读=RU;读已提交=RC(互联网常用)
phantom-read
🎯 骨架
定义:同一事务两次执行相同范围查询,第二次出现"新行"或"少行"(被其他事务 INSERT/DELETE)
vs 不可重复读:幻读关注行数变化(INSERT/DELETE 引起);不可重复读关注值变化(UPDATE 引起)
RR 下解决方案:
快照读:MVCC + ReadView,看历史快照不受新插入影响
当前读:Next-Key Lock(间隙锁 + 记录锁)锁住范围,阻止 INSERT
RC 下不解决:没有间隙锁,当前读会幻读
业务兜底:唯一索引 + DuplicateKeyException 拦截,比依赖 DB 隔离级别更可靠
⚠️ 易混淆
"RR 解决了所有幻读" ❌:仅快照读完全解决;当前读靠间隙锁解决(不是 RR 本身的能力)
间隙锁会带来死锁风险,互联网常宁愿降级 RC + 业务兜底
唯一索引等值查询不需要间隙锁(唯一性保证不会被插入相同值)
🎤 面经
[B站]R1 #2755 数据库事务隔离级别会出现什么问题?幻读是什么?项目中怎么保障不出现幻读?
答:幻读=范围查询行数变化;项目用 RR + 唯一索引 + 业务兜底
[百度]R2 #7175 MVCC 版本号怎么变化?幻读怎么解决?
答:trx_id 全局递增;快照读 MVCC 不可见 + 当前读 Next-Key Lock 阻止插入
[快手]R1 #2730 什么是幻读,MySQL 如何避免幻读?什么是间隙锁,在什么隔离级别触发?
答:范围查询行数变化;MVCC + 间隙锁;间隙锁仅 RR 下当前读触发
[快手]R2 #6333 什么是脏读幻读不可重复读?
答:脏读=未提交 / 不可重复=值变 / 幻读=行数变
[蚂蚁]R1 #6701 脏读和幻读是什么?
答:脏读=读到未提交的修改;幻读=两次范围查询行数不同
[滴滴]R4 #5431 RR 怎么解决幻读?间隙锁为何导致死锁?举具体例子?
答:MVCC + Next-Key Lock;两事务各持 Gap Lock 后 INSERT,循环等待
undo-log-version-chain
🎯 骨架
undo log 双重作用:
事务回滚(A 原子性):记录逆操作(INSERT 的 undo 是 DELETE)
MVCC 多版本:每行隐藏列 roll_pointer 串成版本链,老版本存在 undo log
行隐藏列:trx_id(最后修改的事务)+ roll_pointer(指向上一个旧版本)
版本链查找:当前 trx_id 不可见 → 沿 roll_pointer 跳到旧版本 → 重复判断直到找到可见版本
Purge 清理:trx_id 小于所有活跃 ReadView 的 min_trx_id 才能回收 → 长事务阻塞 Purge
History list length:监控指标,>10000 警惕版本链膨胀(长事务/死事务/Purge 不及时)
⚠️ 易混淆
undo log 不是 redo log 的子集——redo log 物理日志(崩溃恢复),undo log 逻辑日志(回滚 + 多版本)
长事务 vs 死事务症状相同(History list length 高),区分靠 INNODB_TRX 是否有活跃事务
缩容场景不产生长事务(强杀/优雅关闭都会触发回滚),但放大已有慢事务问题
🎤 面经
[百度]R2 #7192 redo log 和 undo log 在数据库中的作用
答:redo 崩溃恢复,undo 事务回滚 + MVCC 多版本
[字节]R1 #4827 MySQL 的三个日志 binlog/redolog/undolog 分别是什么作用
答:binlog 主从+恢复,redo 崩溃重做,undo 回滚+MVCC
[字节]R1 #4745 一个事务执行流程开始到最后这几个日志需要进行什么操作
答:写操作前先写 undo log(防回滚) → 改 Buffer Pool → 写 redo log → commit 时写 binlog → 2PC 提交
[阿里]R4 #6567 MySQL 的 undolog/redolog/binlog 的作用
答:同上三件套,重点说 undo 既保 A 原子性又支撑 MVCC
[百度]R2 #7175 MVCC 版本号怎么变化?
答:trx_id 全局递增写入行隐藏列;旧版本存 undo log,roll_pointer 串版本链
[携程]R1 #7265 redolog/undolog/两阶段提交
答:两阶段提交:prepare → binlog fsync → commit,保证 redo 和 binlog 一致
snapshot-vs-current-read
🎯 骨架
快照读:普通 SELECT,走 MVCC 读历史版本,不加锁
当前读:DML(UPDATE/DELETE/INSERT)+ SELECT FOR UPDATE + SELECT LOCK IN SHARE MODE,读最新版本,加锁
加锁类型:
FOR UPDATE:排他锁(X 锁)
LOCK IN SHARE MODE:共享锁(S 锁)
DML:自动加 X 锁
典型场景:
查询展示 → 快照读(性能好,不阻塞写)
转账查余额 → 必须当前读(怕看到过期值导致超扣)
RR 隔离级别下:当前读触发 Next-Key Lock 防幻读
⚠️ 易混淆
普通 SELECT 永远不加锁,无论 RC/RR
"RR 防幻读"指快照读不幻读;当前读还是会,靠间隙锁
资金扣减必须用 SELECT ... FOR UPDATE,不然 MVCC 给的是历史版本不是当前余额
🎤 面经
[蚂蚁]R1 #6719 当前读与快照读的区别?
答:快照读=普通 SELECT,MVCC 读历史不加锁;当前读=FOR UPDATE/DML,读最新加锁
[蚂蚁]R1 #6821 聚簇索引和非聚簇索引;当前读和快照读
答:同上,重点是转账查余额必须当前读
[京东]R1 #5936 Redis 和数据库一致性/读写并发/当前读和快照读
答:缓存一致性场景下,DB 改完后是否立刻可见取决于读类型
[跨境支付][R1] 间隙锁是 RR 自动加的吗?
答:错,需要"RR + 当前读"才加,普通 SELECT 走 MVCC 快照不加锁
[百度]R2 #7175 MVCC 怎么实现?快照读和幻读关系?
答:MVCC 实现快照读;快照读不幻读,但当前读会
acid-properties
🎯 骨架
A 原子性 Atomicity:事务要么全成功要么全回滚 → undo log 逆向回滚保证
C 一致性 Consistency:数据从一个合法态转到另一个合法态 → A+I+D + 应用层约束共同保证(结果而非手段)
I 隔离性 Isolation:并发事务互不干扰 → MVCC(快照读)+ 锁(当前读)保证
D 持久性 Durability:提交后即使宕机数据不丢 → redo log WAL 保证(先写日志再写数据)
ACID 是单机事务保证;跨节点要靠 2PC/TCC/Saga 等分布式事务方案
⚠️ 易混淆
C 一致性不靠某个具体机制,是 A+I+D + 业务约束的"结果"
I 不是用一种机制实现,而是 MVCC + 行锁 + 表锁 + 间隙锁 组合保证
持久性不等于"立刻刷盘",可以异步刷盘但必须有 redo log 保证崩溃恢复
🎤 面经
[拼多多]R3 #6946 事务的四个特性
答:ACID:原子性、一致性、隔离性、持久性
[小米]R1 #3651 事务的 ACID,每一项是怎样保证的?
答:A=undo log / D=redo log WAL / I=MVCC+锁 / C=A+I+D 的结果+业务约束
[小米]R3 #3157 MVCC;事务的 ACID,每一项是如何保证的?
答:MVCC 实现 I 中的快照读;A 靠 undo log,D 靠 redo log,C 是综合结果
[蚂蚁]R1 #6693 ACID CAP BASE 理论
答:ACID 单机事务;CAP 分布式三选二(一致性/可用性/分区容错);BASE 是 CAP 弱化(最终一致)
[小米]R3 #7231 用到了 MySQL,说了下 ACID 原则和索引
答:同上,重点强调每一项如何保证
S3 锁与并发控制
gap-lock-mechanism
🎯 骨架
定义:锁住索引上相邻记录之间的间隙(开区间),阻止任何事务在此插入
触发条件:仅 RR 下 + 当前读(FOR UPDATE / DML)才加,普通 SELECT 不加
作用:解决 RR 下当前读的幻读问题
Next-Key Lock:Gap Lock + 右边的 Record Lock(左开右闭区间)
Insert Intention Lock:Gap Lock 的子类型,INSERT 操作专用,Gap ↔ Gap 兼容、Gap ↔ Insert Intention 不兼容(间隙锁死锁根源)
⚠️ 易混淆
普通 SELECT 永远不加间隙锁,无论 RR/RC
RC 下没有间隙锁,因此没有间隙锁死锁但有幻读
唯一索引等值查询不加间隙锁(唯一性保证不会插入相同值,无需保护)
间隙锁 ≠ 行锁,是锁住"间隙"这个虚拟空间,不是某行
🎤 面经
[快手]R1 #2730 什么是间隙锁,在什么隔离级别以及什么情况下触发?
答:RR 下当前读触发,作用是阻止其他事务在范围内插入,解决幻读
[蚂蚁]R4 #4150 RR 下幻读问题如何解决?间隙锁死锁原因与排查思路?
答:MVCC + Next-Key Lock;死锁根因 Gap ↔ Insert Intention 不兼容
[快手]R2 #6348 间隙锁是什么?解决了什么问题?为什么要有间隙锁?
答:锁间隙阻止 INSERT,解决 RR 下当前读的幻读
[字节]R1 #6996 MySQL 事务、隔离级别、幻读、间隙锁
答:一套连环:RR + 间隙锁 = 解决幻读
[拼多多]R1 #685 MySQL 有哪些锁?这些锁分别用在哪些场景下?
答:行锁/表锁/间隙锁/Next-Key Lock/Insert Intention,间隙锁仅 RR 当前读
[美团]R4 #1333 MVCC 间隙锁
答:MVCC 解决快照读幻读,间隙锁解决当前读幻读
deadlock-troubleshoot
🎯 骨架
★ 互斥、占有并等待、不可剥夺、循环等待 → 四个同时满足才死锁
★ InnoDB 有自动死锁检测(wait-for graph),检测到后自动回滚代价较小的事务,另一个继续执行
★ SHOW ENGINE INNODB STATUS → 找 LATEST DETECTED DEADLOCK 区块
★ 看两个事务各自持有什么锁、等待什么锁 → 分析加锁顺序是否形成环路
☆ 间隙锁死锁:RR 下两个事务各自 FOR UPDATE 查空加 Gap Lock(兼容),然后都 INSERT 等对方释放 → 死锁
☆ 加锁顺序不一致:事务 A 先锁 id=1 再锁 id=2,事务 B 先锁 id=2 再锁 id=1 → 循环等待
★ 统一加锁顺序(多行操作按 id 从小到大)
☆ 缩短事务,减少持锁时间
☆ 间隙锁死锁频繁 → 考虑降级到 RC(RC 下查空不加 Gap Lock)
☆ 业务层捕获死锁异常(SQLState 40001)自动重试
⚠️ 易混淆
jstack 是 Java 线程死锁检测,不是 MySQL 死锁排查,两个不要混
间隙锁死锁根因:RR 下 FOR UPDATE 查空仍加 Gap Lock,RC 下查空不加锁
🎤 面经
[字节][R1] 数据库死锁怎么排查?→ SHOW ENGINE INNODB STATUS,看 LATEST DETECTED DEADLOCK
[蚂蚁][R1] 间隙锁死锁原因?→ RR 下两事务各加 Gap Lock 后都 INSERT,循环等待
[Shopee][R1] 死锁四个必要条件?→ 互斥、占有并等待、不可剥夺、循环等待(口诀:互占不循)
[美团][R1] 如何预防死锁?→ 统一加锁顺序 + 缩短事务 + 捕获异常重试
[小红书][R1] gap lock 死锁如何规避?→ 降级 RC(查空不加 Gap Lock)或统一加锁顺序
[蚂蚁][R1] InnoDB 检测到死锁怎么处理?→ wait-for graph 检测,自动回滚代价小的事务
[快手][R1] 两行 SQL 造一个死锁?→ 事务A锁id=1等id=2,事务B锁id=2等id=1,加锁顺序相反
for-update-lock-rules
🎯 骨架
触发:当前读,加排他锁 X
加锁范围(RR 下):
主键/唯一索引等值命中 → Record Lock(仅锁这一行)
主键/唯一索引等值未命中 → Gap Lock(锁右端点的间隙)
普通索引等值 → Next-Key Lock + 右边 Gap Lock + 回表对主键加 Record Lock
范围查询 → 扫描路径每条记录加 Next-Key Lock + 回表 + 锁范围比 SQL 条件偏大
无索引 → 全表 Next-Key Lock,锁全表 → 雪崩风险
加锁范围(RC 下):只锁命中行,无 Gap Lock
典型场景:
资金扣减:SELECT balance FROM account WHERE id=? FOR UPDATE
防超卖:SELECT stock FROM product WHERE id=? FOR UPDATE
风险:
长事务持锁阻塞其他写
RR 下间隙锁死锁
无索引导致表锁雪崩
⚠️ 易混淆
FOR UPDATE 必须在事务里(BEGIN/COMMIT),单独执行不生效
普通索引加锁必须回表对主键加 Record Lock(防绕过普通索引直接改行)
锁定的范围比 SQL 条件偏大(扫描路径所有 Gap)
🎤 面经
[网易]R1 #3260 SELECT FROM UPDATE 是什么效果?事务你平常怎么处理?
答:当前读 + X 锁;事务中用,COMMIT 后释放
[快手]R2 #6340 SELECT FOR UPDATE 用过吗,什么意思?
答:加排他锁防并发修改,资金扣减/库存防超卖必用
[蚂蚁]R1 #3951 锁一行数据怎么写?FOR UPDATE 可能出现的问题?
答:SELECT WHERE id=? FOR UPDATE;问题:长事务、间隙锁死锁、无索引锁全表
[蚂蚁]R1 #3450 MySQL 事务,找错误,题目中 SQL 没加 FOR UPDATE
答:资金/库存场景必须 FOR UPDATE,普通 SELECT 走 MVCC 是历史值会超扣
[京东]R1 #5493 悲观锁 FOR UPDATE,写出 SQL 实现
答:BEGIN; SELECT * FROM t WHERE id=? FOR UPDATE; UPDATE t SET ... WHERE id=?; COMMIT;
[跨境支付][通用] 间隙锁是 RR 自动加的吗?
答:错,需要 RR + 当前读(FOR UPDATE / LOCK IN SHARE MODE)才触发
optimistic-pessimistic-db
🎯 骨架
悲观锁:先加锁再操作,依赖 DB 锁机制。SQL:SELECT ... FOR UPDATE
乐观锁:不加锁,提交时校验版本号。SQL:UPDATE t SET col=?, version=version+1 WHERE id=? AND version=?
选型原则:
冲突频繁 → 悲观锁(避免大量重试)
冲突低 → 乐观锁(无锁开销,并发好)
乐观锁两种校验方式:
版本号:每次 UPDATE 加 1
时间戳:用更新时间做校验
缺点:
悲观锁:持锁期间阻塞其他事务,长事务危险
乐观锁:高冲突下大量重试,CPU 浪费,需配合重试上限
⚠️ 易混淆
MySQL 没有专门的"乐观锁"语法,是通过 UPDATE WHERE 校验实现的
SELECT FOR UPDATE 是悲观锁的实现方式,本质是 X 锁
乐观锁不是 CAS(CAS 是 CPU 指令),但思路相似(先读后比较再更新)
🎤 面经
[快手]R2 #6373 说说数据库的乐观锁和悲观锁
答:悲观锁=FOR UPDATE 加 X 锁;乐观锁=版本号校验,UPDATE WHERE version=?
[蚂蚁]R1 #6740 乐观锁和悲观锁的区别?这两种锁在 Java 和 MySQL 分别怎么实现?
答:MySQL:FOR UPDATE / 版本号;Java:synchronized/Lock vs CAS/AtomicInteger
[蚂蚁]R1 #6819 乐观锁和悲观锁
答:选型:冲突高悲观,冲突低乐观
[蚂蚁]R1 #6903 加锁有几种方式,悲观锁乐观锁
答:表/行/间隙/意向锁是悲观锁;版本号是乐观锁
[京东]R1 #5493 说下乐观锁,悲观锁(SELECT FOR UPDATE),并写出 SQL 实现
答:乐观:UPDATE t SET col=?, version=version+1 WHERE id=? AND version=?
[美团]R1 #3105 CAS 在 MySQL 中的应用,不是乐观锁,是 MySQL 源码里哪些地方用了 CAS?
答:InnoDB 的 latch(轻量级锁)实现用了 CAS,比如自增锁的优化
intention-lock
🎯 骨架
定义:表级锁的一种,分 IS(意向共享)和 IX(意向排他),InnoDB 自动加
作用:让"表级锁"操作 O(1) 判断是否冲突,不用扫每一行看是否有行锁
触发:
加行级 S 锁前自动加表级 IS 锁
加行级 X 锁前自动加表级 IX 锁
兼容矩阵:IS ↔ IS / IS ↔ IX / IX ↔ IX 都兼容;与表级 S/X 锁互斥
设计目的:实现"行锁和表锁共存"——表锁能快速感知行锁存在,无需逐行检查
⚠️ 易混淆
意向锁是表级的,不是行级
IX 之间完全兼容,不影响行级并发
意向锁不直接参与业务,是 InnoDB 内部的协调机制
🎤 面经
[B站]R3 #3315 MySQL 行级锁,表级锁。意向锁加锁时机是什么?项目中有没有使用过意向锁?
答:加行锁前自动加意向锁;项目里不直接用,InnoDB 自动管理
[字节]R2 #370 MySQL 锁机制:行锁、间隙锁、意向锁,结合实际场景说明(高并发下单)
答:高并发下单:行锁锁库存行,意向锁支持表锁快速判断,间隙锁防幻读
[拼多多]R1 #685 MySQL 有哪些锁?分别用在哪些场景?
答:行锁/表锁/意向锁/间隙锁/Next-Key Lock;意向锁是行锁和表锁的桥梁
S4 日志与持久化
redo-log-wal
🎯 骨架
★ WAL 核心:先写 redo log(顺序写)再写数据页(异步),随机写→顺序写,性能提升 100x
★ redo log 是 InnoDB 引擎层物理日志(页X 偏移Y 改为Z),环形覆盖,用于崩溃恢复
★ 落盘策略:1=每次提交 fsync(最安全)/ 2=write 不 fsync(MySQL 崩溃不丢)/ 0=后台线程每秒(最快)
binlog 是 Server 层逻辑日志,追加不覆盖,用于主从复制+备份
两阶段提交(2PC)保证 redo log 和 binlog 一致:prepare → binlog fsync → commit
崩溃恢复:扫 redo log 找 prepare 状态 → 查 binlog 有则 commit,无则 undo log 回滚
⚠️ 易混淆
redo log 是物理日志(幂等,可重做多次);binlog 是逻辑日志(非幂等,不能做 crash recovery)
落盘策略 2 vs 0:2 是 write 到 OS Page Cache(MySQL 崩溃不丢),0 是留在 Log Buffer(MySQL 崩溃丢 ~1s)
互联网标准:innodb_flush_log_at_trx_commit=1 + sync_binlog=1(双1配置)
🎤 面经
[蚂蚁][R1] redo log 落盘策略有哪些?各适用什么场景?→ 1=金融双1,2=互联网折中,0=日志统计
[字节][R1] redo log 的作用和工作原理?→ WAL 机制,顺序写日志保证持久性,宕机后重做恢复
[小红书][R1] WAL 机制是什么?→ 先写日志再写数据页,顺序写代替随机写,性能提升 100x
[腾讯][R1] redo log 和 binlog 的区别?→ 引擎层物理日志 vs Server 层逻辑日志,崩溃恢复 vs 主从复制
[京东][R1] redo log 和 undo log 的作用?→ redo 崩溃恢复(重做),undo 事务回滚(逆操作)
binlog-replication
🎯 骨架
binlog 是 Server 层日志(不是 InnoDB 引擎层),所有引擎都有
三种格式:
Statement:记录原始 SQL,省空间但 NOW()/UUID() 等非确定函数主从不一致
Row:记录每行变化,最大但最准确(互联网默认)
Mixed:自动选择(不建议)
三种用途:
主从复制(基础)
数据恢复(基于时间点恢复 PITR)
数据订阅(Canal 模拟从库消费 binlog)
写入策略 sync_binlog:
0:OS 决定何时刷盘,最快但不安全
1:每次事务提交都 fsync,最安全(双 1 配置)
N:累计 N 次事务再 fsync
2PC 中的角色:和 redo log 协同,prepare → 写 binlog → commit,崩溃恢复时是判定依据
⚠️ 易混淆
binlog 是逻辑日志,不能做崩溃恢复(非幂等),崩溃恢复靠 redo log
binlog 追加写不覆盖,按文件分割(mysql-bin.000001 等)
Statement 格式 RC 隔离级别下主从不一致,互联网必须 Row 格式
🎤 面经
[字节]R2 #4806 binlog 和 redolog
答:Server 层逻辑日志 vs 引擎层物理日志;用途/格式/落盘策略全都不同
[Shopee]R1 #2773 binlog 日志是 master 推的还是 slave 来拉的?
答:从库主动连主库(IO Thread 拉),不是主库推
[字节]R1 #4827 MySQL 三个日志 binlog/redolog/undolog 分别是什么作用
答:binlog 主从+恢复,redo 崩溃重做,undo 回滚+MVCC
[美团]R1 #3103 MySQL 主从复制实现原理
答:三线程:Dump Thread(主库发)+ IO Thread(从库收 binlog 写 relay log)+ SQL Thread(重放)
[字节]R1 #4745 一个事务执行流程开始到最后这几个日志需要进行什么操作
答:undo → redo prepare → binlog → redo commit
[阿里]R4 #6567 MySQL 的 undolog/redolog/binlog 的作用
答:三件套各司其职
two-phase-commit
🎯 骨架
背景:redo log(InnoDB 引擎层)和 binlog(Server 层)都需要落盘,必须保证两者一致,否则主从数据不一致 / 崩溃恢复失败
三阶段流程:
prepare 阶段:写 redo log 标记为 prepare 状态 + fsync
写 binlog:Server 层写 binlog + fsync
commit 阶段:把 redo log 改为 commit 状态
崩溃恢复逻辑:
扫 redo log 找到 prepare 状态的事务
该事务对应 binlog 已写完 → 提交(commit)
该事务对应 binlog 未写 → 回滚(undo log 逆操作)
核心保证:redo log 和 binlog 状态一致 → 主从一致 + 崩溃恢复正确
互联网双 1 配置:innodb_flush_log_at_trx_commit=1 + sync_binlog=1 → 强制每次事务提交都 fsync
⚠️ 易混淆
这是 MySQL 内部 2PC(单机),不是分布式 2PC
prepare 阶段后,redo log 已落盘但事务未提交;只有 commit 后才对其他事务可见
binlog 不能用于崩溃恢复(逻辑日志非幂等),所以靠 redo log + 2PC
🎤 面经
[美团]R1 #3100 两阶段提交了解么 MySQL 的?
答:MySQL 内部 2PC,保证 redo log 和 binlog 一致
[蚂蚁]R1 #6820 事务的二段提交机制?
答:prepare(redo log fsync)→ binlog fsync → commit(redo log 改 commit 状态)
[携程]R1 #7265 redolog undolog 两阶段提交
答:三件套配合:undo 回滚 + redo 崩溃恢复 + 2PC 保 binlog 一致
[腾讯]R2 #3123 持久化手段,执行 update 语句时如何保证数据持久化?
答:WAL:先写 undo log → 改 Buffer Pool → 写 redo log(prepare)→ 写 binlog → commit
[字节]R2 #4806 binlog 和 redolog
答:区别+协作:靠 2PC 保两者一致
master-slave-replication
🎯 骨架
三线程架构:主库 Binlog Dump Thread 发送 binlog → 从库 IO Thread 收并写入 relay log → 从库 SQL Thread 读 relay log 重放到数据
relay log 的意义:解耦网络 IO 和 SQL 执行,两者并行互不阻塞;是临时缓冲区,SQL Thread 执行完自动清理
三档复制模式:异步(默认,主库不等从库)/ 半同步(等至少 1 个从库 ACK,金融场景)/ 全同步(等所有从库,几乎不用)
延迟主因:SQL Thread 单线程重放,跟不上主库多线程写入
延迟处理方案:异步 + 写后读走主库(时间窗口路由,互联网常用)/ 半同步 / MySQL 5.7+ 多线程并行复制(基于 group commit 的事务分组)
并行复制分组:同一组 binlog(同一次 group commit 中提交的事务)可在从库并行重放,因为它们在主库上不互相依赖
⚠️ 易混淆
谁发起连接?从库主动连主库,不是主库推过来(dump 线程是主库侧的发送线程,但 TCP 连接是从库发起)
半同步的 ACK 阈值:等 1 个从库 ACK 就算成功,不是所有从库
binlog 格式和主从一致性:Row 格式才能保证 RC 隔离级别下主从一致;Statement 格式在 RC 会出问题(SQL 重放顺序问题)
🎤 面经
[美团 R1] mysql 主从复制实现原理?→ 三线程架构:Dump Thread + IO Thread + SQL Thread
[百度 R2] mysql 主从复制的过程?→ 从库 IO Thread 连主库,主库 Dump Thread 发 binlog,从库写 relay log,SQL Thread 重放
[小红书 R1] 从库拉到 binlog 转成 relay log 准备重放时,执行是单线程还是多线程?为什么?→ MySQL 5.6 前单线程,5.7 后支持多线程并行复制(基于 group commit 分组)
[小红书 R1] 多线程执行时怎么区分不同 group 去复制?→ 同一次主库 group commit 提交的事务,它们的 last_committed 相同,从库按 last_committed 分组并行
[蚂蚁 R4] 主从复制优缺点?→ 优:读写分离提升读吞吐;缺:异步复制有延迟可能丢数据,SQL Thread 单线程成瓶颈
[B站 R1] 主从在延迟情况下如何确保一致性?→ 强一致读走主库(写后读);半同步 ACK;应用层 GTID 等待
S5 分库分表与优化
sharding-strategy
🎯 骨架
拆分维度:
垂直拆分:按字段拆(冷热分离),如订单详情拆出附属表
水平拆分:按行拆(数据量大),如订单按 user_id Hash 分库
水平拆分键选择:
Hash:均匀分布,但范围查询要扫所有分片
Range(按时间):范围查询友好,但热点集中(最新数据全压新分片)
一致性 Hash:节点扩缩容影响小(用于缓存场景多)
分片键选错的坑:订单分库按 user_id 后,按订单号查需扫所有库 → 二次索引表或基因法(订单号尾部嵌入 user_id 的 Hash 后几位)
跨片查询难题:JOIN/聚合/排序难做,常用方案:广播查询、宽表冗余、ES 异构索引
常见分表数量:单表行数 1000w 警戒,2000w 必拆;分 8/16/32/64 库(2 的幂方便扩容)
⚠️ 易混淆
分库 != 分表,分库是物理实例隔离,分表是同库内多张表
分库分表前提:单表/单库性能撑不住,否则不要过早分(增加复杂度)
拆分后唯一 ID 不能再用 auto_increment,要全局唯一 ID(雪花算法)
🎤 面经
[美团]R1 #3092 订单表分库分表后,怎么同时支持按商家和美团订单号查询,避免读扩散?
答:二次索引表:商家 ID → 订单号映射;或基因法把 user_id 嵌入订单号
[蚂蚁]R4 #3425 分库分表需要注意的有哪些?
答:分片键选择 / 跨片查询 / 全局 ID / 数据迁移 / 灰度切流 / 反查映射
[蚂蚁]R1 #6745 订单表有做拆分么,怎么拆?水平拆分后查询过程描述?哈希取模会有什么问题?拆分后主键怎么保证唯一?
答:水平拆按 user_id Hash;扩容时 rehash 难(用一致性 Hash 或预分片);雪花算法
[B站]R1 #2754 如何实现数据库切流(分库分表、主从同步);如何实现无损切流?
答:双写阶段 → 灰度读 → 全量读 → 停老库;离线对账 + 异常重放
[网易]R1 #3281 分库分表应该怎么分?怎么解决数据迁移的问题?
答:选 Hash/Range;迁移:双写 + 数据校验 + 灰度切读
[京东]R2 #3070 几亿网页数据如何分库分表?最常见的查询是某类别的访问量
答:按访问量主键 Hash 分库;类别查询走聚合/异构 ES
distributed-id
🎯 骨架
背景:分库分表后 auto_increment 失效,需全局唯一 ID
雪花算法(Snowflake)64 位结构:
1 位符号位(恒 0)
41 位时间戳(毫秒,可用 69 年)
10 位机器 ID(5 位数据中心 + 5 位机器,最多 1024 台)
12 位序列号(同一毫秒内最多 4096 个 ID)
优点:趋势递增(B+ 树插入友好)、本地生成无网络开销、全局唯一
缺点:
时钟回拨问题:NTP 校时把时间往回调 → 可能生成重复 ID
机器 ID 分配复杂(K8s 动态部署需中心化分配)
替代方案:
美团 Leaf(数据库号段 + 双 buffer + 雪花)
Redis INCR
UUID(无序、长度 36 字符、B+ 树插入差,不推荐主键)
⚠️ 易混淆
雪花算法不是严格递增(不同机器之间可能交错),是"趋势递增"
41 位时间戳是相对时间(项目启动时间为基线),不是 UNIX 时间
时钟回拨处理:检测到回拨 → 等待时间追平 / 抛异常 / 用备用机器位
🎤 面经
[腾讯]R2 #3146 ID 生成器怎么实现的,如何实现全局递增?
答:雪花算法:时间戳+机器ID+序列号;本地趋势递增;时钟回拨需处理
[美团]R1 #4910 雪花算法。雪花算法生成 ID 的逻辑?
答:64 位三段:41 时间戳 + 10 机器 + 12 序列号
[字节]R1 #4831 Redis 还有什么使用?基于 K8s 部署的自动雪花算法机器中心维护
答:K8s 动态机器需中心化分配 worker_id,可用 Redis/ZK 注册表+心跳
[网易]R1 #1608 雪花算法的原理
答:64 位结构 + 趋势递增 + 时钟回拨处理
[字节]R3 #397 雪花算法原理
答:同上
[蚂蚁]R1 #6745 拆分后主键怎么保证唯一?Snowflake 生成的 ID 是全局递增唯一么?
答:不是严格递增(多机器交错),是趋势递增;唯一性靠 时间戳+机器ID+序列号 三段
slow-query-optimization
🎯 骨架
从监控/APM 拿到慢 SQL(生产默认开启慢查询日志)
分类根因(四类):
查询条件异常:IN 过多、数据量过大、深分页
查询逻辑异常:未走索引、排序 filesort、回表多
间接受影响:DB CPU/IO 高、冷数据读入污染 Buffer Pool
瓶颈问题:表量级千万级,需分库分表
针对"查询逻辑异常"用 EXPLAIN 分析:看 type/key/rows/Extra
⚠️ 易混淆
type=ALL 全表扫描必须优化;type=index 全索引扫描也差
排序多列方向不一致(ORDER BY a DESC, b ASC)会触发 filesort,即使有索引
间接受影响类:返回数据不多但扫描行数巨大,或 DB 水位高时正常 SQL 也被拖慢
🎤 面经
[字节 1面] MySQL 慢查询如何优化?→ 分四类根因:条件异常/逻辑异常/间接影响/瓶颈,对症处理
[Shopee 1面] 慢查询怎么发现、排查、优化?→ 监控拿慢 SQL → 分类根因 → EXPLAIN 分析逻辑异常 → 对症优化
[携程 1面] explain 中的 type 从快到慢是什么?→ const > eq_ref > ref > range > index > ALL
[滴滴 3面] explain 重点关注哪里?→ type 是否 ALL、key 是否 NULL、Extra 有无 filesort/temporary
[网易 1面] explain type 字段各值含义?→ ALL 全表、index 全索引、range 范围、ref 非唯一等值、eq_ref 唯一等值、const 主键单行
explain-usage
🎯 骨架
作用:查看 SQL 执行计划,判断索引是否生效、扫描行数、连接方式
重点字段:
type:访问类型,从快到慢:system > const > eq_ref > ref > range > index > ALL
key:实际用到的索引(NULL = 没走索引)
rows:预估扫描行数,越小越好
Extra:Using index(覆盖索引)/ Using filesort(需排序)/ Using temporary(临时表)
常用场景:慢查询优化 → EXPLAIN 看 type 是否 ALL、key 是否 NULL、rows 是否过大
⚠️ 易混淆
key 是实际用的索引,possible_keys 是候选索引,两者可能不同
Extra: Using index 是好事(覆盖索引),Using filesort 是坏事(需优化)
🎤 面经
[字节 1面] MySQL 的 EXPLAIN 用过吗?→ 用于查看执行计划,重点看 type/key/rows/Extra 四个字段
[携程 1面] explain 中的 type 从快到慢是什么?→ const > eq_ref > ref > range > index > ALL,ALL 是全表扫描要避免
[滴滴 3面] explain 怎么使用,重点关注哪里?→ 重点看 type(是否 ALL)、key(是否 NULL)、rows(是否过大)、Extra(是否 filesort)
[网易 1面] explain 执行计划中 type 字段有哪些值?→ system/const/eq_ref/ref/range/index/ALL,ref 是普通索引查询,range 是范围查询
[字节 2面] 使用 explain 命令时需要关注哪些字段?→ type/key/rows/Extra,type=ALL 或 key=NULL 说明没走索引需优化
[阿里 4面] explain 具体怎么看?→ type 看扫描方式,key 看用了哪个索引,rows 看扫描量,Extra 看有无 filesort/temporary
sql-optimization
🎯 骨架
定位慢 SQL:开启慢查询日志(slow_query_log),用 EXPLAIN 分析执行计划
索引优化:type 是否 ALL/index,key 是否 NULL,rows 是否过大
常见优化手段:
加索引(覆盖索引避免回表)
避免索引失效(函数/隐式转换/前缀%/OR)
分页优化(深分页用 keyset:WHERE id > last_id)
大表 JOIN 小表,驱动表走索引
强制走索引:FORCE INDEX(idx_name)
⚠️ 易混淆
索引密集度低(区分度低)时,MySQL 可能选择全表扫描而不走索引
覆盖索引(Extra: Using index)是好事,不需要回表
🎤 面经
[快手 2面] 做过哪些 MySQL 优化?→ 慢查询日志定位 → EXPLAIN 分析 → 加索引/覆盖索引/避免失效/分页优化
[百度 2面] 关键 SQL 优化怎么做的?为什么性能不好?→ 先看执行计划,type=ALL 说明全表扫,加索引或改写 SQL
[蚂蚁 1面] SQL 优化的方式有哪些?→ 索引优化、避免索引失效、分页优化、JOIN 优化、减少回表
[快手 1面] 线上 SQL 优化经验?MySQL 怎么选索引?→ 基于统计信息选最优索引,可用 FORCE INDEX 强制指定
[快手 2面] 聚集索引和非聚集索引了解吗?了解回表吗?→ 聚集索引叶子存数据行,非聚集存主键值;回表=二级索引查到主键再查聚集索引
[百度 2面] 数据索引密集度很差,优化意义不大吧?→ 对,区分度低的列不适合建索引,MySQL 会选全表扫;应建在高区分度列上
REDIS
S1 内存数据库的诞生
redis-data-structures
🎯 骨架
底层 SDS,解决 C 字符串三个问题:O(1) 取长度 / 预分配减少 realloc / 二进制安全
三种编码:int(纯数字)/ embstr(≤44 字节,一次 malloc)/ raw(>44 字节,两次 malloc)
扩容策略:< 1MB 翻倍,≥ 1MB 每次 +1MB
编码变化不可逆:embstr 任何修改 → raw,int 做字符串操作 → raw
双索引:skiplist 按 score 有序(范围查询 O(logN+M))+ hashtable 按 member 查分数 O(1)
跳表:有序链表 + 多层索引,随机层高(P=0.25,期望 1.33),不需要旋转
span 字段:记录跨度,用于 O(logN) 计算 ZRANK 排名
小数据量用 ziplist:member-score 交替存放按 score 有序,O(n) 遍历插入
转换阈值:entries > 128 或 value > 64 字节 → skiplist+hashtable,不可逆
小数据量 ziplist(连续内存,key-value 交替,查找 O(n))
大数据量 hashtable(dict 结构,链表法解决冲突,查找 O(1))
转换阈值:entries > 128 或 value > 64 字节,不可逆
渐进式 rehash:ht[0]+ht[1] 两个表,每次操作搬一个桶
rehash 期间:读两表都查,写只写 ht[1]
触发条件:负载因子 > 1 扩容(BGSAVE 期间 > 5 才强制)
全整数且数量少 → intset(有序整数数组,二分查找 O(logN))
有非整数或超过 512 个 → hashtable(value=NULL 只用 key)
intset 底层有序是实现优化(为了二分),Set 语义上无序
intset 升级机制:插入更大范围整数时整个数组重新编码(int16→int32),不可逆
⚠️ 易混淆
embstr 和 raw 的区别是 malloc 次数(1 vs 2),不是功能区别
SDS 扩容是 realloc(可能原地扩展也可能新分配+拷贝),不是一定新建对象
ZSCORE 走 hashtable O(1),不走跳表;ZRANGEBYSCORE 走跳表,不走 hashtable
跳表不用红黑树三个原因:范围查询天然高效 / 实现简单 200 行 / 并发友好局部修改
ziplist 下 ZADD 已存在 member 改 score:先删后插两次 memmove
Redis rehash 渐进式(分摊到每次操作),Java HashMap 一次性全量搬迁
BGSAVE 期间推迟扩容是为了避免 fork COW 内存翻倍
链表法头插,不是尾插
intset 有序 ≠ Set 有序,SMEMBERS 返回顺序不保证
intset 升级不可降级
🎤 面经
[拼多多][R3] SDS 动态字符串的优点?→ len 字段 O(1) 取长度;free 预分配减少 realloc;靠 len 不靠 \0 结束,二进制安全
[滴滴][R1] SDS 优点?为什么不同长度用不同结构体?→ embstr 一次 malloc 对象头和 SDS 连续,CPU 缓存友好;raw 两次 malloc 因为大字符串扩容频繁,分开管理更灵活
[字节][R2] ZSet 怎么实现的?跳表原理?为什么不用红黑树?→ skiplist+hashtable 双索引;多层有序链表随机层高 O(logN);范围查询链表直接遍历 / 实现简单 / 并发友好
[字节][R1] 跳表查询过程?时间复杂度?→ 从最高层开始,forward < 目标就前进,> 目标就下降;查找/插入/删除 O(logN),范围 O(logN+M)
[美团][R1] ZSET 底层 ziplist 和跳表?→ 小数据 ziplist 连续内存省空间 O(n);大数据 skiplist+hashtable O(logN)+O(1);128/64 阈值自动切换不可逆
[拼多多][R1] Redis Hash 怎么实现?rehash 和 Java HashMap 区别?→ 渐进式每次搬 1 桶 vs 一次性全量;Redis 单线程不能卡,Java 多线程可以短暂卡
[阿里][R1] Hash 冲突怎么处理?扩容会阻塞吗?→ 链表法头插;不阻塞,渐进式 rehash 单次微秒级
[美团][R1] 字典结构?负载因子?→ dict 维护 ht[0]/ht[1],负载因子=used/size,>1 扩容 >5 强制 <0.1 缩容
[蚂蚁][R1] 用过哪些数据结构,什么场景?→ Set 用于标签去重、共同好友(SINTER 交集)、抽奖(SRANDMEMBER)
skiplist-principle
🎯 骨架
★ 跳表 = 有序链表 + 多层索引,用随机化代替旋转保持平衡
★ 不是多条链表,是一批节点各有多层指针(level[] 柔性数组)
☆ Redis 用 p=0.25(每个节点有 25% 概率多一层),最高 32 层
★ 从最高层开始,forward.score < target 就前进,否则下降(i--)
★ "下降"不是回到 header,是同一个节点从 level[i] 换到 level[i-1]
☆ 查找条件是严格小于,循环结束后 current 是前驱,forward 是目标
★ 查找/插入/删除:O(logN);范围查询:O(logN+M)
★ skiplist 负责按 score 有序存储,范围查询 O(logN+M)
★ hashtable 负责按 member 精确查找 O(1)(ZSCORE/ZRANK)
☆ 小数据量用 ziplist(128个entry/64字节以下),超过阈值转 skiplist+hashtable,不可逆
★ 范围查询天然高效:底层有序链表直接遍历,红黑树需中序遍历
☆ 实现简单:~200行 vs 红黑树 ~500行(旋转+变色)
☆ 并发友好:局部修改,细粒度锁;红黑树旋转涉及多节点
⚠️ 易混淆
ZRANGEBYSCORE 走跳表 O(logN+M),ZSCORE 走 hashtable O(1),不是所有操作都走跳表
跳表靠随机化保持平衡(概率上),不是严格平衡;红黑树靠旋转严格平衡
🎤 面经
[字节][R2] ZSet 怎么实现的?跳表原理?→ skiplist+hashtable 双索引;多层有序链表随机层高 O(logN)
[字节][R2] 为什么不用红黑树?→ 范围查询链表直接遍历更高效;实现简单;并发友好
[字节][R1] 跳表查询过程?时间复杂度?→ 从最高层开始,forward<目标就前进否则下降;O(logN)
[小红书][R1] SkipList 能否越两级搜索?→ 可以,高层索引跨度大,一次前进可跨越多个节点
[阿里][R1] Redis 底层数据结构,跳表实现原理?→ 多层有序链表,随机层高,查找从高层到低层逐步逼近
[得物][R1] 举一个幻读的例子?MySQL 如何解决幻读?→ (跳表无关,此题属于 MySQL 模块)
[滴滴][R1] 跳表原理以及数据结构?→ 有序链表+多层索引,每层是下层的子集,查找 O(logN)
io-multiplexing
🎯 骨架
★ IO 多路复用:一个线程监听多个 fd,有事件就绪才处理,避免每个连接一个线程
★ select/poll:遍历所有 fd(O(n)),select 有 1024 上限,poll 无上限但仍 O(n)
★ epoll:红黑树管理 fd + 就绪链表,O(1) 获取就绪事件,支持百万连接
Redis 用 epoll(Linux):单线程 + IO 多路复用,避免上下文切换,性能极高
ET(边缘触发)vs LT(水平触发):ET 只通知一次(需一次读完),LT 持续通知(更安全)
epoll_create/epoll_ctl/epoll_wait 三个系统调用
⚠️ 易混淆
select 的 1024 限制是 fd_set 的 bit 数组大小,可重编译修改但不推荐
epoll 不是"没有遍历",而是只遍历就绪的 fd(就绪链表),不是全量遍历
Redis 单线程指网络 IO 和命令执行是单线程,持久化/异步删除是多线程
🎤 面经
[腾讯][R1] select/poll/epoll 的工作原理和区别?→ select O(n)+1024限制,poll O(n)无限制,epoll O(1)红黑树+就绪链表
[滴滴][R1] Redis 性能核心是什么?IO 多路复用原理?→ 单线程+epoll,避免上下文切换,O(1) 获取就绪事件
[美团][R2] epoll 的结构?ET 和 LT 模式?→ 红黑树(管理fd)+就绪链表(就绪事件);ET 边缘触发一次,LT 水平持续通知
[Shopee][R1] IO 多路复用如何实现?→ 内核监听多个 fd,有事件就绪放入就绪链表,用户态 epoll_wait 取走处理
[阿里][R3] select、poll 和 epoll 的对比,epoll 事件通知实现原理?→ 内核回调机制,fd 就绪时回调函数将其加入就绪链表
redis-why-fast
🎯 骨架
纯内存操作:所有数据在内存,读写纳秒级,不需要磁盘 IO
单线程命令执行:无锁,无上下文切换,避免多线程竞争开销(Redis 6.0+ 网络 IO 多线程,命令执行仍单线程)
高效数据结构:SDS(动态字符串)、跳表、压缩列表、哈希表等,针对不同场景优化
IO 多路复用:epoll 事件驱动,单线程管理大量连接,不阻塞
性能瓶颈:单线程 CPU 密集操作(大 key 操作、复杂命令);网络带宽;持久化(AOF fsync)
⚠️ 易混淆
Redis 6.0 多线程是网络 IO 多线程,命令执行仍然单线程(保证原子性)
单线程不是 Redis 快的唯一原因,纯内存才是核心
持久化会影响性能:AOF always 每条 fsync,everysec 每秒 fsync
🎤 面经
[字节 R1] Redis 为什么快?单线程模型有什么缺点?→ 纯内存+单线程无锁+epoll;缺点:CPU 密集操作阻塞所有请求
[B站 R2] Redis 为什么这么快?→ 纯内存+高效数据结构+IO 多路复用+单线程无竞争
[京东 R1] Redis 为什么快?→ 同上,补充:避免了线程切换和锁竞争开销
[阿里 R2] Redis 速度快的原因,性能瓶颈分析,Redis 6.0 多线程模型?→ 瓶颈在网络 IO,6.0 用多线程处理网络读写,命令执行仍单线程
[腾讯 R1] Redis 速度快的原因是什么?性能瓶颈在哪里?→ 纯内存+单线程;瓶颈:网络带宽/大 key/持久化
redis-6-multithread
🎯 骨架
Redis 6 之前:单线程处理命令(网络 IO + 命令执行都在主线程)
Redis 6 引入多线程:只用于网络 IO 读写,命令执行仍是单线程
原因:网络 IO 是瓶颈(带宽打满),命令执行本身很快,多线程执行反而引入锁竞争
⚠️ 易混淆
Redis 6 多线程 ≠ 命令并发执行,命令执行依然串行,保证原子性
默认多线程 IO 是关闭的,需要配置 io-threads 开启
🎤 面经
[阿里 2面] Redis 速度快的原因,性能瓶颈分析,Redis 6.0 多线程模型?→ 速度快:内存+单线程无锁+IO多路复用;6.0 多线程只处理网络IO,命令执行仍单线程
[蚂蚁 2面] Redis 是单线程还是多线程?→ 命令执行单线程,6.0 起网络 IO 多线程,两者分离
[滴滴 3面] Redis 是单线程还是多线程?既然单线程为什么能处理大量读请求?→ 单线程+IO多路复用,epoll 一个线程监听所有连接,无上下文切换开销
[腾讯 5面] Redis 6.0 为解决什么瓶颈引入多线程?→ 网络 IO 瓶颈,带宽打满时命令执行等网络,引入多工作线程处理网络读写
[京东 1面] Redis 单线程为什么快?多线程会有什么问题?→ 单线程无锁无竞争;多线程需要加锁保证原子性,反而引入竞争开销
[快手 2面] Redis 为什么用单线程?用多线程会不会更好?→ 命令执行本身是内存操作极快,瓶颈在网络IO不在CPU;多线程执行命令需要加锁,得不偿失
[Shopee 1面] Redis 是单线程还是多线程?→ 6.0 前全单线程;6.0 后网络IO多线程,命令执行仍单线程
S2 缓存攻防战
cache-penetration
🎯 骨架
查不存在的数据,缓存和 DB 都没有,每次打到 DB
三板斧:空值缓存 / 布隆过滤器 / 参数校验
空值缓存:null 也缓存,设短 TTL,简单但占内存
布隆过滤器 = bit 数组 + K 个哈希函数
全 1 = 可能存在;任一 0 = 一定不存在(误判单向)
写入链路必须 BF.ADD 同步,否则新数据被拦截
口诀:穿透查空、击穿热点、雪崩批量
⚠️ 易混淆
穿透(查不存在)vs 击穿(热点 key 过期)vs 雪崩(批量过期/Redis 挂)
误判方向:会把"不存在"判为"存在",不会把"存在"判为"不存在"
布隆不支持删除(多元素共享 bit 位),需要删除用 Cuckoo Filter
🎤 面经
[腾讯]R1 #3112 如何解决缓存穿透、缓存雪崩问题?布隆过滤器的底层原理是什么?误判率如何产生?
答:穿透用空值缓存+布隆,雪崩加随机 TTL;布隆=bit 数组+K 哈希,碰撞导致误判
[腾讯]R3 #3139 缓存穿透,怎么解决?
答:空值缓存(短 TTL)+ 布隆过滤器前置拦截 + 参数校验
[滴滴]R3 #3000 介绍下缓存击穿、缓存雪崩、缓存穿透是什么?解决方案?
答:穿透查空用布隆/空值;击穿热点用互斥锁/不过期;雪崩批量加随机 TTL
[滴滴]R4 #5449 深入 Redis,缓存穿透如何解决
答:答:布隆过滤器拦在缓存前 + 空值缓存设短过期 + 接口参数校验
[美团]R1 #4907 为什么会出现缓存穿透的问题?
答:恶意请求查不存在的 key(如 id=-1),缓存永远 miss,每次打 DB
[京东]R1 #5476 缓存穿透和缓存雪崩
答:穿透=查不存在数据用布隆+空值;雪崩=大量 key 同时过期用随机 TTL+预热
[字节]R1 #4830 缓存穿透、缓存击穿等等,二级三级缓存
答:穿透用布隆;击穿用互斥锁;多级缓存=本地 Caffeine + Redis 分流
[携程]R1 #7255 缓存穿透、击穿、雪崩
答:三者本质区别在"查空/热点/批量";分别对应布隆+空值/互斥锁/随机 TTL
cache-avalanche
⚠️ 易混淆
雪崩 = 大量 key 同时失效(批量);击穿 = 单个热点 key 失效(单点)
穿透是恶意攻击或 bug,查的 key 根本不存在
🎤 面经
[滴滴 1面] 介绍缓存击穿、雪崩、穿透及解决方案?→ 击穿=热点key过期用互斥锁;雪崩=批量过期用随机TTL;穿透=不存在key用布隆过滤器
[腾讯 1面] 如何解决缓存穿透、缓存雪崩?布隆过滤器底层原理?→ 穿透用布隆过滤器(多个哈希函数映射bit数组,有误判率);雪崩用随机TTL
[滴滴 3面] 缓存击穿、雪崩、穿透的区别?→ 击穿单点热key,雪崩批量key,穿透不存在的key
[京东 1面] 缓存穿透和缓存雪崩?→ 穿透=查不存在的数据;雪崩=大量key同时过期
[美团 1面] 缓存雪崩概念及应对措施?→ 大量key同时过期导致DB压力骤增;随机TTL打散过期时间,或多级缓存兜底
[拼多多 1面] 缓存穿透击穿雪崩区别和方案?→ 穿透/布隆过滤器,击穿/互斥锁,雪崩/随机TTL+限流
cache-breakdown
🎯 骨架
定义:热点 key 过期瞬间,大量并发请求穿过缓存打到 DB(单 key + 高并发 + 刚过期)
三大方案:互斥锁 / 逻辑过期 / 永不过期 + 异步刷新
互斥锁:setnx 抢锁,一个查 DB 其他等待重试 → 一致性强、RT 不稳定(锁等待)
逻辑过期:value 内嵌 expireTime,过期返回旧值 + 异步重建 → 可用性高、刷新窗口期都是旧值
互斥锁实现要点:setnx + TTL 防死锁,Redisson RLock + 看门狗续期防业务超时
业内组合:Redisson 分布式锁 + Caffeine 本地缓存兜底 + Sentinel 热点参数限流
选型:资金/权限/状态类用互斥锁(怕脏数据);商品详情/排行榜/配置用逻辑过期(要 RT)
⚠️ 易混淆
缓存击穿 vs 穿透 vs 雪崩:击穿=单热点 key 过期瞬间;穿透=查 DB 也没有的 key;雪崩=大批量 key 同时过期或 Redis 宕机
口诀:穿透查空、击穿热点、雪崩批量
互斥锁 vs 逻辑过期:前者抢锁失败的线程阻塞等新值,后者抢锁失败/成功都返回旧值;逻辑过期窗口期内所有线程都拿旧数据
击穿 ≠ 长期热点 key 高 QPS:击穿强调"过期瞬间"的并发回源;稳态热点 key 治理用本地缓存(Caffeine)+ 多级缓存
重建业务简单的 key 风险低:回源 RT 短,DB 抗一波就过去了;真正怕的是重建慢的复杂查询
🎤 面经
[百度]R2 #7193 缓存击穿是什么?如何解决
答:单热点 key 过期瞬间并发打 DB;三选一:互斥锁、逻辑过期、永不过期 + 异步刷
[滴滴]R3 #3000 介绍下缓存击穿、缓存雪崩、缓存穿透是什么?解决方案?
答:击穿单点过期/穿透查空/雪崩批量过期;对应互斥锁、布隆过滤+空值缓存、TTL 加随机
[美团]R1 #4909 缓存击穿,重建业务不复杂的 key 是没有缓存击穿风险是吗?热点 key 含义
答:重建快回源压力小风险低;热点 key 指单 key QPS 远高于平均,需特殊治理
[字节]R2 #4810 缓存击穿、穿透、雪崩
答:三件套对比,关键词:单热点/查空/批量;方案分别互斥锁、布隆过滤、TTL 抖动+多级缓存
[蚂蚁]R1 #4166 redis 相关,缓存击穿、雪崩、并发锁竞争这些
答:击穿配互斥锁,锁本身要防超时和误删,Redisson RLock + 看门狗续期是标准实现
[字节]R1 #4830 热点 key 定义,如何缓存热点 key,缓存穿透/击穿等,二级三级缓存
答:热点 key=访问极频繁;多级缓存 Caffeine+Redis 把热点请求拦在本地,隔离击穿
[字节]R1 #4862 缓存穿透,缓存击穿,缓存雪崩的概念及解决方案
答:三件套口诀:穿透查空(布隆/空值)、击穿热点(互斥锁/逻辑过期)、雪崩批量(TTL 随机+高可用)
[携程]R1 #7256 多级缓存为什么能解决这个问题
答:本地缓存(Caffeine)TTL 短于 Redis,热点 key 大部分请求被本地拦截,Redis 过期时回源压力被分摊
bloom-filter
🎯 骨架
原理 = bit 数组(长度 m)+ K 个哈希函数;插入把 K 个位置全置 1,查询 K 位全 1=可能存在,任一为 0=一定不存在
误判率 = 哈希碰撞导致"假阳性",与 m / K / 已插入数 n 相关;m 越大、K 适中越低,工业默认约 1%
合理设大小:按预估元素数 n + 期望误判率 p 公式算 m 和 K(m ≈ -n·lnp/(ln2)²,K ≈ m/n·ln2),Guava/Redisson 构造方法直接传 n 和 fpp
不支持删除:多元素共享 bit 位,删一个会误删其他(要删用 Counting BF 或 Cuckoo Filter 替代)
Redis 实现:RedisBloom 模块(BF.ADD/BF.EXISTS,支持自动扩容)或 Redisson RBloomFilter(底层 bitmap + murmur3,分布式共享)
Guava 实现:单机内存版,murmur3_128 双哈希模拟 K 个哈希(hash1 + i·hash2),底层 LockFreeBitArray(AtomicLongArray + CAS)
写入链路必须同步:DB 新增数据要同时 BF.ADD,否则新数据会被拦截(启动全量加载只覆盖存量)
⚠️ 易混淆
只有"假阳性"(不存在判存在),没有"假阴性"(存在判不存在)—— 方向是单向的
不能删除元素(普通 BF),要删用 Counting BF(位变成计数器)或 Cuckoo Filter
Guava 的"K 个哈希"实际只算 2 个 murmur3 哈希,用 hash1+i·hash2 组合模拟(Kirsch-Mitzenmacher 优化),不是真的算 K 次
选型:单机 < 1 亿用 Guava;分布式共享用 Redisson RBloomFilter;高 QPS / 需要扩容用 RedisBloom 模块
误判率不是越低越好,越低 → m 越大 → 内存越多,1% 是工程平衡点
🎤 面经
[腾讯]R1 #3112 如何解决缓存穿透、缓存雪崩问题?布隆过滤器的底层原理是什么?其误判率是如何产生的?
答:bit 数组+K 哈希;K 位全 1=可能存在,任一为 0=一定不存在;哈希碰撞导致假阳性
[B站]R1 #5838 布隆过滤器,底层原理,如何合理设置大小,如何设置误差?
答:按预估元素数 n + 期望 fpp 算 m 和 K,Guava 构造方法直接传 n 和 fpp,工业默认 1%
[美团]R1 #4935 布隆过滤器原理(判断不存在就一定不存在,判断存在就一定存在吗)
答:不存在一定不存在,存在不一定存在;只有假阳性没有假阴性
[百度]R2 #7191 布隆过滤器原理
答:bit 数组+K 哈希函数;插入置 K 位为 1,查询 K 位全 1 才可能存在
[字节]R1 #567 缓存击穿、穿透、布隆过滤器那一套
答:穿透=查不存在数据,BF 前置拦截;击穿=热 key 过期用互斥锁;雪崩=过期时间加随机
[拼多多]R3 #6926 布隆过滤器有了解过吗?
答:防缓存穿透;bit 数组+K 哈希;有误判率约 1%,不支持删除
[拼多多]R1 #1106 布隆过滤器有了解过吗?
答:同上;分布式用 Redisson 或 RedisBloom,单机用 Guava
[腾讯]R3 #3139 缓存穿透,怎么解决?
答:空值缓存(短 TTL)+ 布隆过滤器前置拦截 + 参数校验三件套
cache-consistency
🎯 骨架
Cache Aside(旁路缓存)= 业界标准:读穿透 + 写「先更 DB 再删缓存」
为什么删而不更:删是幂等的,避免「A 写 1 → B 写 2 → B 更新缓存 → A 更新缓存」时序覆盖
为什么先 DB 后缓存:先删后更会被并发读用旧值回填,不一致窗口大;先更后删的窗口需要「缓存恰好失效 + 读写时序交错」,概率极低
延迟双删:删 → 更 DB → sleep N ms → 再删一次,sleep ≥ 主从延迟 + 读 RT + 回填 RT,经验 100-500ms
Canal 监听 binlog 异步删缓存:和业务解耦、可靠、可重放,但引入 MQ 要解决重复消费 + 消息丢失
强一致路线:分布式锁(Redisson)锁住 key 期间禁止并发读写,性能损失大,金融场景才用
集群删除失败兜底:MQ 重试 + 短 TTL 兜底过期 + 监控告警(删缓存失败要可观测)
最终一致性容忍度:业务能接受秒级脏读就用 Cache Aside;不能接受就走 DB 直读或锁
⚠️ 易混淆
强一致 vs 最终一致:强一致 = 读必读到最新写(要锁/同步阻塞);最终一致 = 短窗口可能读到旧值,最终收敛
「先删缓存再更 DB」vs「先更 DB 再删缓存」:前者并发不一致概率高(读请求把旧值回填),后者概率低(要缓存恰好失效)
双写一致性 ≠ 缓存一致性:双写指 Redis + DB 两边都成功,分布式下不能原子,不等于强一致
删缓存失败 vs 更新 DB 失败:删失败靠 MQ 重试 + TTL 兜底;DB 失败直接回滚不动缓存
Write Through 在分布式下不是真强一致:Redis 和 DB 不在同一个事务里,仍可能不一致
🎤 面经
[B站]R1 #5842 Redis 和 MySQL 的数据一致性,先删缓存再更新数据库,延迟双删,异步 canal 中间件更新
答:标准方案 Cache Aside「先更 DB 再删缓存」;并发场景叠加延迟双删兜底;最可靠是 Canal 监听 binlog 异步删缓存
[携程]R1 #1792 接口一致性。延时双删:定 sleep 时间、最终一致性、如何强一致性、分布式锁、用什么做的分布式锁
答:sleep ≥ 主从延迟 + 读 RT + 回填 RT,经验 100-500ms 压测定值;强一致用 Redisson 分布式锁锁 key
[携程]R1 #1698 缓存和 DB 的一致性?延迟在哪?del update 延迟再删?binlog 好处?
答:延迟来自主从复制 + 读回填;binlog 好处是和业务解耦、不漏事件、可重放、不侵入代码
[Shopee]R1 #5412 update 时怎么避免缓存和 DB 不一致?先删除 Redis 缓存可能导致什么问题?
答:先更 DB 再删缓存;先删缓存会被并发读用 DB 旧值回填,导致缓存长期是旧值
[美团]R1 #4908 缓存和数据库的一致性。为什么不直接 update 缓存而是删除缓存。拷打强一致 vs 最终一致
答:删缓存幂等,避免乱序写覆盖;强一致要锁性能差,电商场景接受秒级最终一致用 Cache Aside
[拼多多]R1 #3407 缓存一致性?canal binlog mq 具体过程?怎么防止重复消费?怎么防止消息丢失?
答:DB 写 → binlog → Canal 解析 → MQ → 消费者删缓存;幂等 key 防重;ACK 机制 + 死信队列防丢
[字节]R1 #432 集群模式下缓存无法成功删除,缓存未成功删除时有哪些策略?
答:删失败重试 + MQ 异步补偿 + 短 TTL 兜底过期 + 监控告警,最终靠过期实现最终一致
[蚂蚁]R1 #6731 Redis 的数据一致性问题
答:Cache Aside「先更 DB 再删缓存」是主线,叠加延迟双删/Canal 解决并发不一致,强一致才用锁
delayed-double-delete
🎯 骨架
延迟双删 = 删缓存 → 更 DB → sleep N ms → 再删一次
解决「先更 DB 再删缓存」时缓存刚失效 + 读写时序交错的并发不一致
sleep 公式:≥ 主从同步延迟 + 读 DB RT + 回填 Redis RT + 业务安全余量
经验值 100-500ms 覆盖 99% 场景,线上靠压测或耗时采样定值
太短:第二次删时读请求还没回填完,没用
太长:不一致窗口变大,用户体验差
更可靠的替代:Canal 监听 binlog 异步删缓存,和业务解耦、可重放、不漏事件
⚠️ 易混淆
延迟双删 vs 单纯先删后更:双删是「删→更→sleep→再删」,第一次删是预防雪崩失效,第二次删才是关键
sleep 时间不是拍脑袋 100ms,要包含主从延迟(读写分离架构的核心因素)
双删失败仍可能不一致,必须叠加 TTL 兜底;强一致只能上分布式锁
🎤 面经
[B站]R1 #5842 缓存和 MySQL 一致性,先删缓存再更新数据库,延迟双删,异步 canal 中间件更新
答:标准用 Cache Aside「先更 DB 再删缓存」;并发场景叠加延迟双删;最可靠用 Canal 监听 binlog
[携程]R1 #1792 延时双删:定 sleep 时间、最终一致性、如何强一致性、分布式锁、用什么做的分布式锁
答:sleep ≥ 主从延迟 + 读 RT + 回填 RT,经验 100-500ms;强一致用 Redisson 分布式锁锁 key
[携程]R1 #1698 缓存和 DB 的一致性?延迟在哪?del update 延迟再删?binlog 好处?
答:延迟来自主从复制 + 读回填;binlog 好处是和业务解耦、不漏事件、可重放、不侵入代码
[携程]R1 #1792 延迟双删 sleep 时间怎么定?
答:覆盖主从同步延迟和读回填,经验 100-500ms;线上靠耗时采样或压测定值
[腾讯]R5 #7129 怎么保证缓存和 DB 之间的数据一致性
答:Cache Aside「先更 DB 再删缓存」为主线,叠加延迟双删/Canal;强一致才用分布式锁
[蚂蚁]R1 #6731 Redis 的数据一致性问题
答:旁路缓存 + 延迟双删 + Canal binlog 三层方案,强一致才上锁,性能差
local-cache
🎯 骨架
本地缓存 = 进程内缓存(Caffeine/Guava),无网络 IO,纳秒级访问
本地缓存 vs Redis:本地快但不一致、容量小;Redis 慢但分布式共享、容量大
适用场景:极热点 + 读多写少 + 容忍秒级不一致(商品详情、配置、字典)
Caffeine 底层 = ConcurrentHashMap + W-TinyLFU(分窗口统计频次,比 LRU/LFU 更精准)
失效策略:expireAfterWrite(写后过期)、expireAfterAccess(访问后过期)、refreshAfterWrite(异步刷新)
多级缓存架构:Local(10-30s 短 TTL)→ Redis → DB;写时 DB+Redis,再 MQ 广播删 Local
分布式 Local 不一致兜底:MQ 广播主通知 + 短 TTL 自动过期,Pub/Sub 不可靠不能单用
⚠️ 易混淆
Caffeine 用 W-TinyLFU 不是普通 LRU;W-TinyLFU 用 Count-Min Sketch 估计频次,比 LRU 抗扫描
本地缓存「失效」不是「淘汰」:失效 = 过期/被通知;淘汰 = 容量满按算法剔除
多级缓存写流程:先 DB → 再删 Redis → MQ 广播删 Local(Local 必须最后处理,不然窗口期不一致更严重)
Redis Pub/Sub fire-and-forget 不持久化,订阅者断连消息丢,分布式 Local 失效不能只靠 Pub/Sub
🎤 面经
[字节]R2 #4811 本地缓存 caffeine 的底层实现
答:ConcurrentHashMap 存数据 + W-TinyLFU 算淘汰;写后过期/访问后过期/异步刷新三种失效
[腾讯]R2 #3116 本地缓存和 Redis 缓存的区别?本地缓存的场景?Redis 数据更新策略?
答:本地无网络快但单机不一致;用于极热点+读多写少;Redis 走 Cache Aside「先更 DB 再删」
[字节]R1 #568 热 key 失效如何处理?分布式本地缓存数据不同步怎么办?
答:MQ 广播主通知 + 短 TTL 兜底;Redis Pub/Sub 不可靠不能单用
[蚂蚁]R1 #3946 怎么保证分布式项目中分布式缓存和本地缓存的一致性
答:写时先更 DB+删 Redis,MQ 广播让各节点删 Local;Local 设短 TTL 兜底
[拼多多]R1 #858 如何保证本地缓存 + Redis + DB 的数据一致性?
答:写链路 DB → 删 Redis → MQ 广播删 Local;读穿透回填,三层 TTL 递减
[字节]R1 #4830 热点 key 定义,如何缓存热点 key,二级三级缓存
答:热点 = 单 key QPS 暴增;本地缓存兜底分流;二级 = Local + Redis,三级加 CDN
[滴滴]R3 #2530 本地缓存怎么保证数据一致性?
答:MQ 主动失效通知 + 短 TTL 兜底,写后异步刷新;不能用 Pub/Sub 唯一机制
[得物]R1 #5801 设计缓存时考虑热点 key、数据一致性,本地缓存分布式缓存怎么设计
答:Local + Redis 双层,热点走 Local 短 TTL;MQ 广播失效;穿透用布隆+空值
S3 持久化与高可用
rdb-aof-persistence
🎯 骨架
RDB = 定时全量快照(dump.rdb),fork 子进程 + COW,主线程不阻塞
AOF = 写后追加命令日志,三种刷盘 always/everysec(生产默认)/no
RDB 优点:文件小、恢复快(直接加载二进制);缺点:两次快照间数据可能丢
AOF 优点:丢失窗口小(everysec 最多丢 1s);缺点:文件膨胀、恢复慢
混合持久化(4.0+ 默认开):AOF 重写时先写 RDB 基线再追加 AOF 增量,恢复 = 加载 RDB + 重放少量 AOF
同时开启重启优先加载 AOF(数据更全),AOF 损坏 redis-check-aof 修复
AOF 重写 bgrewriteaof:fork 子进程生成最小命令集,原子替换,期间双写到旧 AOF + 重写缓冲区
⚠️ 易混淆
AOF 是「写后日志」(命令执行成功才记),与 MySQL redo log 写前日志(WAL)相反
fork 不是复制数据是复制页表,10GB 实例 fork 仍可能 100-500ms 阻塞
everysec 不等于无影响:上次 fsync 没完成会阻塞主线程 write(Linux 行为)
AOF 同时开 + 0 字节 AOF → 重启加载空库 RDB 被忽略(监控 AOF 文件大小突降)
🎤 面经
[腾讯]R3 #3131 Redis 持久化策略?RDB 和 AOF
答:RDB 定时全量快照恢复快;AOF 追加命令丢失少;4.0+ 混合持久化兼顾
[快手]R2 #2753 RDB 是怎么做的?AOF 是怎么做的?
答:RDB fork 子进程 COW 写快照;AOF 命令追加,每秒 fsync 刷盘
[快手]R2 #6334 RDB 和 AOF 同时开启,默认使用哪种方式恢复,为什么?
答:优先 AOF;因为 AOF 丢失窗口小数据更全,混合持久化下 AOF 头部就是 RDB
[百度]R2 #2822 RDB 快照会影响目前线程执行任务嘛?
答:BGSAVE 用子进程,主线程不阻塞;fork 瞬间页表复制有毫秒级阻塞
[字节]R1 #6139 AOF 文件过大怎么办?如何重写?
答:bgrewriteaof 触发重写;fork 子进程读内存生成最小命令集,重写缓冲区双写后原子替换
[拼多多]R3 #6967 Redis 持久化?为什么要用 RDB + AOF
答:RDB 恢复快但丢数据;AOF 丢失少但恢复慢;组合 = 快照基线 + 增量日志
[滴滴]R1 #2522 Redis 的 AOF 和 RDB 区别,能配合使用吗?
答:能,4.0+ 混合持久化默认开;AOF 文件头是 RDB 二进制,尾部是增量命令
[Shopee]R1 #2774 Redis 持久化有哪几种方式,怎么选?
答:纯缓存可不开;要数据安全用 AOF everysec;要快速恢复+小丢失用混合持久化
[网易]R1 #3258 AOF 文件长久下来会很大怎么办?
答:定时 bgrewriteaof 重写压缩;配置 auto-aof-rewrite-percentage 自动触发
sentinel-cluster
🎯 骨架
Sentinel = 高可用方案(自动故障转移),1 主 N 从 + 哨兵进程,全量数据每节点存
Cluster = 高可用 + 水平扩展,多主多从分片,16384 哈希槽 CRC16(key) % 16384
Sentinel 故障转移:主观下线 SDOWN → 客观下线 ODOWN(多数派确认)→ Raft 选 Leader → 提升从为主
Cluster 路由两层:key→槽(永不变)/ 槽→节点(可迁移),扩容靠迁移槽在线不停服
ASK = 临时重定向(迁移中,不更新路由表);MOVED = 永久重定向(迁移完,更新路由表)
Hash Tag:{merchantId}:quota 用 {} 内做哈希保证同槽,解决多 key 操作 CROSSSLOT
16384 槽数原因:bitmap 2KB(16384/8),Gossip 心跳包能塞下;槽多包大、槽少分布不均
⚠️ 易混淆
Sentinel 不分片,全量数据每节点都有;Cluster 分片,每节点只存部分槽
Slave 提升为主不需要重新加载数据(已有完整副本),切换延迟来自客户端连接池
客户端「too many redirections」:节点宕机选主期间路由表过期,大量请求重定向触发上限
Gossip 是去中心化最终一致协议,节点信息扩散有延迟;不是强一致
🎤 面经
[拼多多]R3 #6911 Redis 集群和哨兵机制有什么区别?
答:哨兵只做高可用全量主从;集群分片+高可用,16384 槽多主多从
[蚂蚁]R4 #4157 Redis Cluster 中如何保证一致性?gossip 协议原理?符合 CAP 哪个?
答:节点间 Gossip 最终一致;牺牲强一致选 AP;主从异步复制故障转移可能丢数据
[蚂蚁]R4 #4158 Redis Cluster 扩缩容期间是否可以持续提供服务?底层机制?
答:可以,迁移槽期间客户端访问旧节点收 ASK 临时重定向,迁移完收 MOVED 更新路由
[拼多多]R1 #856 Redis Cluster 是如何实现的?一个机器挂了槽位如何处理?
答:CRC16(key)%16384 路由;主挂副本选举升主,槽归属变更通过 Gossip 扩散
[携程]R1 #1831 Redis Cluster 模式下怎么做分片切分?加入新节点会发生什么?
答:CLUSTER MEET 加入 → 迁移部分槽到新节点 → 客户端 ASK/MOVED 自动找新位置
[百度]R2 #2107 Redis Cluster 与 Sentinel 区别
答:Sentinel 解决高可用全量数据;Cluster 解决高可用+水平扩展,按槽分片
[蚂蚁]R1 #6979 RedisCluster 怎么实现节点宕机重选主?slot 迁移过程中数据不在旧节点会发什么指令?
答:副本走 Raft 选举升主;迁移中数据不在旧节点返回 ASK 让客户端去新节点
[腾讯]R2 #3117 Redis 的部署方式,集群部署的丢失数据,主备切换过程
答:单机/主从/哨兵/Cluster;主从异步复制故障转移可能丢未同步数据
[小红书]R1 #4082 Redis 哨兵分布式部署模式会有什么问题?
答:脑裂(min-replicas-to-write 兜底);客户端切换延迟;Sentinel 自身要奇数防分区
memory-eviction
🎯 骨架
过期删除(已设 TTL 的 key 处理):惰性删除(访问时检查)+ 定期删除(每秒 10 次随机抽样)
内存淘汰(内存满触发):8 种策略,按维度分 noeviction / allkeys- / volatile-
默认 noeviction:内存满写入直接报错,读不受影响
业务层最常用 allkeys-lru / allkeys-lfu,TTL 业务用 volatile-lru / volatile-ttl
Redis LRU 是近似 LRU:随机采样 maxmemory-samples(默认 5)个 key 淘汰最旧的
不用真 LRU:标准双向链表百万级 key 额外内存开销巨大,采样几乎零开销
LFU 基于访问频次(lru 字段高 16 位时间 + 低 8 位频次),能识别「曾热已冷」的 key
⚠️ 易混淆
过期策略 ≠ 淘汰策略:过期是 TTL 到了删 key;淘汰是内存满了主动剔除
惰性删除 + 定期删除组合:单独惰性会导致冷过期 key 占内存;单独定期 CPU 高
LRU vs LFU:LRU 看「最近访问时间」,偶尔访问的冷数据不会淘汰;LFU 看「访问频次」,冷数据频次低被淘汰
volatile-* 只在设过期时间的 key 中淘汰,没设 TTL 的 key 不会被淘汰
🎤 面经
[百度]R2 #2823 说一下 Redis 的淘汰策略(LRU),你知道 Redis 的 LRU 怎么实现的嘛?
答:随机采样 5 个 key 淘汰 lru 字段最旧的;近似 LRU 几乎零额外内存
[百度]R2 #5627 Redis 内存淘汰策略?曾经热点后面不是了,LRU 和 LFU 处理有什么区别?
答:LRU 看最近访问时间,冷热点偶尔访问不淘汰;LFU 看频次,冷热点频次衰减后被淘汰
[快手]R2 #6337 Redis 内存满了怎么办?
答:按 maxmemory-policy 触发淘汰策略;默认 noeviction 写报错;常用 allkeys-lru
[网易]R1 #3289 Redis 缓存回收机制,数据过期策略?内存淘汰策略?
答:过期 = 惰性 + 定期;淘汰 = 内存满触发 8 种策略,按 LRU/LFU/Random/TTL 维度组合
[滴滴]R1 #2392 缓存数据的淘汰策略?Redis 内部怎么实现 LFU 或 LRU?
答:redisObject.lru 字段高 16 位访问时间低 8 位频次;采样近似实现
[阿里]R1 #83 实现 LFU 缓存淘汰策略的核心思路?怎么处理访问频率相同的键?
答:频次最小堆/双哈希;频次相同用最近访问时间打破平局
[蚂蚁]R3 #3886 Redis 内存淘汰机制
答:8 种策略覆盖 noeviction/allkeys-lru/lfu/random/volatile-* 维度
[Shopee]R1 #915 Redis 内存满了的淘汰策略有哪些?
答:noeviction(默认报错)+ allkeys/volatile × LRU/LFU/Random/TTL 共 8 种
[美团]R1 #1398 Redis 作为内存缓存,内存不足时的淘汰策略有哪些?
答:默认 noeviction;业务通常 allkeys-lru/lfu;金融场景用 volatile-* 保不能淘汰的 key
S4 分布式锁
setnx-distributed-lock
🎯 骨架
核心命令:SET key value NX EX 30 —— NX 互斥、EX 防死锁、value 防误删
key = 资源标识(如 lock:quota:{merchantId}),value = 持有者标识(UUID 或 clientId:threadId)
锁三要素:互斥(NX)+ 安全(unique value)+ 活性(EX 自动过期防客户端宕机死锁)
释放锁必须 Lua 脚本:先 GET 比 value 再 DEL,原子执行避免误删别人的锁
演进路径:DB 行锁 → SETNX+EXPIRE(非原子有缺陷)→ SET NX EX(一条命令)→ Lua 释放 → Redisson 看门狗 → RedLock
锁粒度按业务资源(商户 ID/订单 ID),太粗吞吐低、太细等于没锁
不要手写:生产直接 Redisson,自动续期 + 可重入 + 公平锁等都封装好
⚠️ 易混淆
SETNX + EXPIRE 两步操作非原子,中间宕机锁不会过期 → 一定要用 SET NX EX 一条命令
value 不能固定,固定值导致 A 锁过期 B 拿到锁后 A 释放误删 B 的锁
直接 DEL 危险,必须 Lua GET+DEL 原子化
业务超时 > 锁过期 → 锁提前释放并发出错;用 Redisson 看门狗续期解决
🎤 面经
[美团]R4 #3984 Redis 和 SETNX 命令是如何实现分布式锁的?
答:SET key value NX EX 30 一条命令保证原子;value 用 UUID 防误删;Lua 释放
[滴滴]R3 #2563 SETNX 如何解决 A 加锁 B 解锁问题?
答:value 用唯一标识;释放前 Lua 脚本校验 value 再 DEL,单线程执行原子
[蚂蚁]R3 #4201 分布式锁的实现,SETNX 怎么支持可重入?怎么避免 key 提前失效?怎么避免单点?
答:可重入用 Hash 计数;过期用看门狗续期;单点用 RedLock 多实例多数派
[字节]R3 #986 Redis 的 SETNX;分布式锁的提供方
答:SETNX 原子标志互斥;提供方有 Redis(Redisson)、ZK(Curator)、Etcd
[携程]R1 #3729 Redis 单线程,分布式锁
答:单线程保证 SETNX 原子;NX 互斥 + EX 防死锁 + Lua 释放是基本盘
[阿里]R2 #31 分布式锁实现
答:SET NX EX + UUID + Lua 释放是基础版;生产用 Redisson 看门狗 + 可重入
[网易]R1 #1609 分布式锁 Redis 命令是什么?为什么要加超时?锁过期后另一线程拿到锁怎么办?
答:SET NX EX;超时防客户端宕机死锁;过期问题用看门狗续期 + 业务幂等
redisson-watchdog
🎯 骨架
看门狗解决「锁过期时间不好定」:业务比锁先到期会并发出错,业务比锁慢锁还在浪费
默认锁 30 秒,看门狗每 10 秒(30/3)检查一次,业务还在就 PEXPIRE 续期到 30s
关键区分:lock.lock() 启动看门狗;lock.lock(10, SECONDS) 手动指定不启动
底层用 HashedWheelTimer 时间轮做定时任务,O(1) 调度
可重入用 Hash 结构:field=clientId:threadId,value=重入计数;HINCRBY 加减
客户端宕机 → JVM 退出 → 看门狗停止续期 → 30s 后锁自动释放,不会死锁
释放锁:计数 -1 归零才 DEL + PUBLISH 通知等待者抢锁
⚠️ 易混淆
看门狗不是「锁永不过期」,是「业务期间持续续期」
手动指定过期时间不启动看门狗:Redisson 尊重你的判断;也防止 bug 卡死无限续期
看门狗续期失败(Redis 抖动 / 网络分区)业务可能被中断 → 业务幂等 + DB 兜底
时间轮 ≠ 简单 ScheduledExecutor:时间轮 O(1) 加减任务,海量定时任务性能远超
🎤 面经
[美团]R1 #3096 看过 Redisson 分布式锁的源码吗?底层怎么实现的?
答:Lua 脚本加锁 + Hash 存重入次数;时间轮看门狗 30s 续 10s 续期
[滴滴]R3 #2986 Redisson 底层实现原理?
答:Lua 脚本保证原子;Hash 支持可重入;HashedWheelTimer 看门狗续期;PubSub 通知等待者
[腾讯]R2 #2174 Redis 分布式锁你们怎么用的(Redisson),原理知道吗?
答:默认 30s 锁 + 看门狗每 10s 续期;Lua 加锁 Hash 计数支持重入
[Shopee]R3 #898 分布式锁如何实现?Redisson 怎么实现的?Lua 脚本?
答:核心 Lua 脚本原子加锁 + Hash 计数;看门狗时间轮续期;解锁 PUBLISH 唤醒
[字节]R1 #979 Redisson 的时间轮了解过吗?说一下原理
答:HashedWheelTimer,环形数组每格一个任务链表,指针每 tick 走一格;O(1) 加任务
[other]R1 #4466 为啥用 Redisson 而不是 SETNX?watchdog 是咋实现的?加锁时 Redis 不可用了咋整?
答:Redisson 自动续期+可重入封装;watchdog 时间轮定时;Redis 不可用降级 DB 兜底
[小红书]R1 #5309 Redisson 分布式锁优化,如何解决锁续期失败导致的业务中断?
答:续期失败抛异常让业务感知;幂等 + DB 唯一约束兜底;监控续期失败告警
[京东]R1 #5940 分布式锁实现/可重入实现/看门狗机制实现/不需要锁续期怎么办
答:可重入用 Hash 计数;看门狗时间轮续期;不需要续期手动指定过期时间,Redisson 不启动
[other]R1 #4426 Redisson(watchdog)解决的是什么问题?拿到锁的机器挂了会有啥问题吗?
答:解决业务超时锁提前释放;机器挂了看门狗停,锁到期自动释放不死锁
redlock
🎯 骨架
解决的问题:Redis 主从异步复制,Master 宕机时锁可能没同步到 Slave,新 Master 上锁丢失
主从锁丢失场景:A 在旧 Master 加锁 → Master 宕机未同步 → Slave 升主 → B 在新 Master 加锁 → 两人同时持有锁
RedLock 算法:5 个独立 Redis 实例(非主从),向所有实例发 SET NX EX
加锁成功条件:超过半数(3/5)成功 + 加锁总耗时 < 锁过期时间
Kleppmann 批评:依赖时钟假设(NTP 跳跃可能锁提前过期),不是真正的共识协议
GC 停顿场景:客户端 STW 卡顿超过锁过期时间,以为自己持有锁但实际已过期,仍可能并发
生产共识:分布式锁是性能优化手段不是数据正确性保障,正确性靠 DB 唯一约束/乐观锁/条件更新兜底
⚠️ 易混淆
RedLock ≠ Redisson 的 RLock:RedLock 是多实例多数派算法;RLock 是单实例锁封装
5 个实例必须独立部署(不是主从),否则失去多数派意义
RedLock 性能比单实例锁低(要 5 次 IO + 多数派确认),生产很少真用
真正强一致需要 ZK/Etcd 这类共识系统,不是 Redis
🎤 面经
[other]R1 #4426 Redis 实现分布锁要考虑哪些?超时时间、Redisson(watchdog)、redlock 解决的是什么问题?
答:RedLock 解决主从异步复制锁丢失;5 实例多数派加锁;生产很少用 DB 兜底更可靠
[蚂蚁]R3 #4201 分布式锁怎么避免单点问题?依赖数据库的实现?
答:RedLock 多实例多数派避单点;DB 唯一约束/乐观锁兜底,正确性不依赖锁
[other]R1 #4466 为啥用 Redisson 而不是 SETNX?加锁的时候 Redis 不可用了咋整?
答:单点用 RedLock 5 实例 3/5 加锁;Redis 不可用降级 DB 兜底,业务幂等
[蚂蚁]R4 #4157 Redis Cluster 中如何保证一致性?符合 CAP 哪些?分布式系统应该 CP 还是 AP?
答:Redis 选 AP,强一致用 ZK/Etcd;锁场景靠 DB 兜底,不依赖 Redis 强一致
[腾讯]R2 #3117 Redis 集群部署的丢失数据,主备切换过程
答:异步复制切换可能丢未同步数据;锁场景靠 RedLock 或 DB 唯一约束兜底
[滴滴]R3 #2986 Redis 分布式锁有了解吗?Jedis、Lettuce 和 Redisson 的适用场景?
答:Jedis 同步阻塞简单场景;Lettuce 异步 Netty 高性能;Redisson 封装 RedLock+ 可重入+看门狗
S5 生产实战
bigkey-hotkey
🎯 骨架
BigKey 判定:String > 10KB / Hash/Set/ZSet/List > 5000 元素 / 单 key > 10MB
BigKey 危害:单线程下操作耗时与数据量成正比,DEL/HGETALL 阻塞所有请求;集群数据倾斜
BigKey 治理:发现(--bigkeys / MEMORY USAGE / 慢查询)→ 拆分(Hash 取模 / List 按时间)→ UNLINK 异步删除
UNLINK vs DEL:DEL 同步释放阻塞主线程;UNLINK 主线程 O(1) 解引用,lazy free 后台线程回收
HotKey 危害:同一 key 只落一个 slot,分片形同虚设;单节点 QPS 暴涨导致雪崩
HotKey 方案:本地缓存(读多写少最优)> Key 打散 key_1/key_2 随机读 > 读写分离从节点分担
发现工具:业务预估(大促/活动)+ 监控告警(单 key QPS)+ redis-cli --hotkeys(需 LFU 模式)
⚠️ 易混淆
大 key 不是「key 名字长」是「value 大」
热 key 分片救不了:Cluster 按 key 哈希分片,同一 key 必落同一节点;要在客户端层(本地缓存)或应用层(key 打散)解决
--bigkeys 用 SCAN 不会长时间阻塞,但每个 key 的 TYPE+大小检查会增加负载,低峰期或从库执行
禁线上 KEYS(O(N) 阻塞),用 SCAN 替代
🎤 面经
[字节]R1 #6176 hotkey bigkey
答:bigkey 拆分 + UNLINK 异步删;hotkey 本地缓存 + key 打散 + 读写分离
[携程]R1 #5276 Redis 热 k 和大 k 问题
答:大 key 拆分到 Hash 多 field/List 按时间分桶;热 key 本地 Caffeine 兜底分流
[字节]R1 #568 热 key 失效如何处理?分布式本地缓存数据不同步怎么办?
答:本地缓存兜底分流;MQ 广播失效 + 短 TTL 解决分布式不同步,Pub/Sub 不可靠
[腾讯]R2 #2175 Redis 热 key 知道吗?单台机访问流量过高怎么办?10 台同时失效如何回源?
答:本地缓存分流;互斥锁单线程回源 + 永不过期+异步刷新避免击穿
[滴滴]R1 #2522 Redis 的 AOF 和 RDB 区别?热 key 问题和大 key 问题怎么解决?
答:热 key = 本地缓存 + 打散;大 key = 拆分 + UNLINK + HSCAN 分批读
[字节]R4 #706 Redis 的热 key 如何解决
答:本地缓存 + 限流 + 预热 + 多级缓存;秒杀场景叠加令牌桶
[蚂蚁]R3 #4241 Redis,64 位 KV 存储,大 key 的场景怎么处理
答:拆分到多 key(Hash 取模/List 时间分桶);UNLINK 异步删;监控 MEMORY USAGE
[字节]R1 #514 Redis 的大 key 问题,为什么会产生大 key
答:序列化对象太大、集合无上限堆积、缓存粒度设计不当;预防靠规范 + CI 检查
[other]R1 #4461 Redis 的热 key 问题遇到过吗?热 key 变成了大 key 怎么办?多大算大 key?
答:String > 10KB 或集合 > 5000 元素算大;热变大场景拆 key + 异步删,本地缓存兜底
[京东]R1 #5909 项目 Redis 淘汰策略用的什么?Redis 大 key 怎么应对?
答:allkeys-lru 通用;大 key 提前发现拆分、UNLINK 删、HSCAN 分批;上线前 CI 校验
redis-project-experience
🎯 骨架
★ 缓存一致性:Cache Aside 模式(先更新 DB 再删缓存),延迟双删兜底最终一致性,删缓存而非更新缓存(避免并发写导致脏数据)
★ 缓存三连击:穿透(查不存在的 key)→ 布隆过滤器/空值缓存;击穿(热点 key 过期)→ 互斥锁/永不过期+异步更新;雪崩(大量 key 同时过期)→ 过期时间加随机值
★ 分布式锁:SETNX + 过期时间,Redisson 看门狗自动续期,锁必须带 value(UUID)防误删
⚠️ 易混淆
为什么不"先更新缓存"?三个坑:
DB 失败/抖动 → 缓存有 DB 没有
事务回滚 → 其他线程读到从未持久化的脏数据
并发写覆盖 → A 先改 DB 但后写缓存,把 B 的新值盖了
所以用 Cache Aside:先写 DB(事务提交后)再删缓存,删而非更新
延迟双删的"延迟"是等主从同步完成,不是随便等几秒
🎤 面经
[拼多多][R1] 如何解决缓存和数据库不一致?→ Cache Aside + 延迟双删,删而非更新
[美团][R1] 缓存穿透击穿雪崩区别和方案?→ 穿透查不存在/布隆,击穿热点过期/互斥锁,雪崩批量过期/随机TTL
[蚂蚁][R2] 分布式锁怎么实现?锁提前过期怎么办?→ SETNX+TTL,Redisson 看门狗续期
MQ
S1 消息管道的诞生
mq-use-cases
🎯 骨架
★ MQ 三大场景:解耦(上下游不直接依赖)、削峰(缓冲流量洪峰)、异步(非核心流程异步化)
★ 为什么不用线程池:线程池是进程内的,服务重启消息丢失;MQ 持久化,跨服务,天然解耦
发 MQ 和接 MQ 可以是同一个应用(自消费),也可以是不同应用(跨服务)
削峰本质:把瞬时高并发请求"存起来",消费端按自己节奏处理,避免数据库被打垮
解耦本质:生产者不关心谁消费,消费者不关心谁生产,通过 topic 解耦
⚠️ 易混淆
削峰 ≠ 填谷(填谷是削峰的附带效果,不单独算一个场景)
线程池 vs MQ:线程池无持久化、无跨服务、无背压,只适合进程内异步
🎤 面经
[通用][R1] MQ 三大使用场景?→ 解耦、削峰、异步
[通用][R1] 不用 MQ 用线程池不行吗?→ 线程池进程内无持久化,服务重启消息丢;MQ 持久化+跨服务+背压
[通用][R1] 发 MQ 和接 MQ 是同一个应用吗?→ 不一定,可以同一个(自消费),也可以不同(跨服务解耦)
[通用][R1] 什么场景下不适合用 MQ?→ 强一致性要求(金融交易)、写多读少(命中率低)、数据量极小
[通用][R1] MQ 削峰的原理是什么?→ 缓冲瞬时流量,消费端按自己节奏处理,避免数据库被打垮
kafka-architecture
🎯 骨架
整体拓扑:Producer → Broker Cluster(Topic→Partition→Replica)→ Consumer Group,元数据由 ZooKeeper(旧)或 KRaft(新)管理
核心概念五件套:
Broker:物理节点,存 Partition 副本
Topic:逻辑分类,跨 Broker 切成多 Partition
Partition:并行度单位,每个 Partition = 一个文件目录(多个 Log Segment)
Replica:副本(1 Leader + N Follower),Leader 处理读写,Follower 异步同步
Consumer Group:消费组,组内一个 Partition 只能被一个 Consumer 消费
元数据管理演进:
0.8 及之前:ZK 管理消费组 + Broker 元数据(Consumer 直连 ZK,羊群效应)
0.9:GroupCoordinator 移入 Broker,Consumer 不再直连 ZK
2.8+:KRaft 模式(Kafka Raft),完全去掉 ZK,元数据走 Raft 日志
Controller:集群里特殊的 Broker,负责分区 Leader 选举、ISR 变更广播、Topic 创建/删除;Controller 也是 Broker,靠 ZK 抢临时节点(或 KRaft 选举)
GroupCoordinator:每个 Consumer Group 一个,所在 Broker = hash(group.id) % __consumer_offsets 分区数,负责心跳、Rebalance、offset 提交
存储结构:Partition = 多个 Log Segment(默认 1GB 切分),每段含 .log + .index(稀疏索引)+ .timeindex
关键内部 Topic:__consumer_offsets(消费位点)、__transaction_state(事务状态)
⚠️ 易混淆
Topic 不是物理实体,Partition 才是;Topic 删除不会立即释放磁盘
Controller 和 GroupCoordinator 是两回事:Controller 管集群元数据,GroupCoordinator 管单个消费组
Consumer Group 内 Consumer 数 > Partition 数,多余 Consumer 空转
Topic 多时(>1000)效率下降:每个 Partition 一个文件,写入退化成随机 IO;RocketMQ CommitLog 解决此问题
Kafka 0.9 之前 offset 存 ZK,0.9 后存 __consumer_offsets 内部 Topic
🎤 面经
[腾讯]R1 #5048 Kafka 的模型架构?
答:Producer→Broker Cluster(Topic→Partition→Replica)→Consumer Group,ZK/KRaft 管元数据
[Shopee]R1 #2790 Kafka partition、broker、consumer、consumer group、topic 都是啥关系?
答:Broker 是物理节点;Topic 切成 Partition 跨 Broker 分布;Consumer Group 内一个 Partition 只能被一个 Consumer 消费
[字节]R1 #4828 创建 Topic 如何将分区放置到不同 Broker?
答:Kafka 按 Broker 数量轮询分配,Replica 错开,避免 Leader 集中在同一台
[Shopee]R1 #2785 Kafka 分区怎么同步的?
答:Follower 主动 Fetch Leader 的数据;HW=min(ISR LEO),超过 lag.time 的 Follower 踢出 ISR
[Shopee]R1 #2783 Kafka 选主怎么做的?
答:Leader 挂了 → Controller 检测 ZK 临时节点消失 → 从 ISR 中选第一个为新 Leader
[滴滴]R1 #2524 GroupCoordinator 选举、消费组协调器选举过程?kafka 默认 topic 干什么用的?
答:Coordinator = hash(group.id) % __consumer_offsets 分区数对应分区的 Leader Broker;默认内部 Topic 存 offset、事务状态
[腾讯]R1 #5048 Kafka 的 100 个节点如果有一两个挂掉了会怎么办?
答:ISR 缩减;Controller 触发受影响 Partition 的 Leader 选举;客户端 metadata 刷新后重连新 Leader
[网易]R2 #4236 为什么 broker 上 topic 越多效率越慢?
答:每个 Partition 独立文件;Topic 多时写入变成随机 IO,PageCache 命中率下降
producer-send-flow
🎯 骨架
5步流程:序列化 → 分区器选 Partition → RecordAccumulator 攒 batch → Sender 线程批量发送 → Broker 接收写 Page Cache
RecordAccumulator 的作用:批量缓冲,攒够 batch.size(默认16KB)或等够 linger.ms(默认0ms)才发,提升吞吐
Sender 线程:独立后台线程,从 RecordAccumulator 取 batch,按 Broker 分组发送,处理响应回调
acks 三档:0=不等确认(最快最不可靠)/ 1=Leader 写入即返回 / all(-1)=ISR 全部写入才返回
Broker 不解压:压缩 batch 直接落盘,消费端解压,零拷贝优势(写用 mmap,读用 sendfile)
默认异步刷盘:写 Page Cache 就返回,靠多副本保证可靠性,不是同步刷盘
生产端幂等:enable.idempotence=true,Broker 用 PID + SequenceNumber 去重,防重复发送
⚠️ 易混淆
acks=0 是"零确认",acks=1 是"Leader 确认",别搞反
RecordAccumulator 是主线程写入,Sender 是独立线程读取,两者解耦
分区策略:有 key → hash 取模;无 key(2.4+)→ Sticky Partitioner(同 batch 发同分区,batch 满再换),不是纯轮询
异步刷盘 ≠ 不可靠,靠 ISR 多副本兜底
🎤 面经
[字节 R3] 说下消息发送、消费过程?→ 序列化→分区→RecordAccumulator→Sender→Broker→Page Cache→Consumer poll
[字节 R3] 讲讲 RocketMQ 事务消息的发送过程?→ 半消息→执行本地事务→commit/rollback→Broker 投递
[美团 R1] MQ 如何保障消息不丢失?从生产、消费、Broker 三个环节分别说明?→ 生产端 acks=all+重试;Broker 多副本+刷盘;消费端手动提交 offset+幂等
[Shopee R1] Kafka 怎么保证不丢消息?→ 三端:acks=all / ISR 多副本 / 手动提交 offset
[字节 R1] 怎么解决网络波动导致的消息重复发送和重复消费?→ 发送:enable.idempotence=true;消费:幂等(唯一键/Redis/去重表)
[小红书 R2] 如何确保消息只被消费一次?→ 消费端幂等:唯一键 DB 去重 / Redis setNX / 业务状态机
[美团 R1] 消息堆积应该怎么处理?→ 事前:限流+容量规划;事中:扩分区+扩消费者并行消费;事后:补偿消息
[other R1] MQ 的顺序消息如何保证有序?→ 同一业务 key 路由到同一分区,单分区内有序;消费端单线程消费该分区
S2 可靠性全链路
reliability-three-ends
🎯 骨架
生产端:同步发送 + 失败重试 + 事务消息(本地事务 + 半消息)
Broker 端:同步刷盘(性能换可靠)+ 多副本 ISR/同步复制
消费端:手动 ACK + 幂等消费(业务唯一键去重)+ 死信队列兜底
⚠️ 易混淆
异步刷盘性能好但宕机丢消息;同步刷盘可靠但吞吐低
消费端 at-least-once 必然重复,幂等是消费端自己的责任,不是 MQ 保证的
🎤 面经
[阿里 4面] RocketMQ 如何保证消息可靠性,从生产端、MQ 端和消费端三方面分析?→ 生产端事务消息/重试 + Broker 同步刷盘/副本 + 消费端手动 ACK/幂等
[other 1面] RocketMQ 如何保证消息不丢?异步发送、异步刷盘还能保证消息不丢吗?→ 不能,异步刷盘宕机会丢;需同步刷盘 + 同步复制才能保证
[字节 1面] 项目中如何解决 Kafka 重复消息和保证消息不丢失?→ 不丢:acks=all + 同步刷盘;不重复:消费端幂等(唯一键/Redis 去重)
[字节 2面] Kafka 消息丢失的场景有哪些?如何保证消息不丢失?→ 生产端 acks=0/1 丢、Broker 异步刷盘宕机丢、消费端自动提交 offset 丢
[网易 1面] 怎么保证消息不丢和不重复消费?→ 不丢靠三端配置,不重复靠消费端幂等
acks-mechanism
🎯 骨架
acks=0:Producer 不等 Broker 确认,发完即走,最快但可能丢消息
acks=1:Leader 写入后确认,Leader 挂了未同步到 Follower 则丢消息
acks=-1(all):Leader + 所有 ISR Follower 都写入后确认,最安全,延迟最高
ISR:In-Sync Replicas,与 Leader 保持同步的副本集合,acks=-1 只等 ISR 确认
⚠️ 易混淆
acks=-1 不是等所有副本,是等 ISR 内所有副本,ISR 可能只有 Leader 自己(min.insync.replicas 控制最小 ISR 数)
acks=1 是默认值,生产环境重要数据建议 acks=-1 + min.insync.replicas=2
🎤 面经
[阿里 1面] Kafka ack 有几种设置,各有什么区别?→ 0/1/-1 三种:0 不确认、1 Leader 确认、-1 ISR 全确认,可靠性依次升高
[通用] acks=-1 一定不丢消息吗?→ 不一定,ISR 只剩 Leader 时退化为 acks=1,需配合 min.insync.replicas≥2
[通用] Kafka 如何保证消息不丢失?→ Producer 端 acks=-1,Broker 端 min.insync.replicas≥2,Consumer 端手动提交 offset
[other 1面] 如果让你实现消息队列,ack 策略怎么设计?→ 参考 Kafka:0/1/all 三档,业务按可靠性 vs 性能取舍
isr-replica-ha
🎯 骨架
★ ISR = In-Sync Replicas,与 Leader 保持同步的副本集合
★ LEO(Log End Offset)= 每个副本当前写到哪了,Leader 和每个 Follower 各自维护
★ HW(High Watermark)= min(ISR 所有副本的 LEO),Consumer 只能读 HW 以下的消息
★ Follower 超过 replica.lag.time.max.ms(默认 30s)没有向 Leader 发 Fetch 请求 → 踢出 ISR
☆ 心跳(session.timeout.ms)= Broker 存活检测,和 ISR 踢出是两回事,不要混
★ min.insync.replicas:写入门槛,ISR 数量不足时直接拒绝写入(NotEnoughReplicasException)
★ 消息可见性靠 HW:写入 Leader 成功 ≠ Consumer 可读,要等 ISR 所有副本 LEO 推进,HW 才推进
☆ min.insync.replicas 和 HW 是独立机制:前者控制写入门槛,后者控制读取边界
☆ Consumer 只从 Leader 读(默认),Follower 只同步不对外提供读服务
☆ 三步:截断到 HW(丢弃 HW 后未提交的脏数据)→ Fetch 追赶 Leader → LEO 追上后重新加入
☆ Leader 掉线 → Controller 检测(ZK 临时节点消失)→ 从 ISR 中选新 Leader
☆ unclean.leader.election.enable=false:禁止非 ISR 副本当选,防止数据截断
⚠️ 易混淆
Follower 掉线 ≠ Leader 选举,只是 ISR 缩减;Leader 掉线才触发选举
acks=-1 单独不够:ISR 缩到只剩 Leader 时退化为 acks=1,必须配合 min.insync.replicas=2
ISR 内同步延迟不会导致数据不一致,只影响 HW 推进速度(Consumer 可见时机)
🎤 面经
[蚂蚁][R1] ISR 是什么?→ 与 Leader 保持同步的副本集合,超 30s 没发 Fetch 请求就踢出
[阿里][R3] Follower 掉线触发 Leader 选举吗?→ 不会,只是 ISR 缩减;Leader 掉线才选举
[网易][R2] ISR 和 HW 关系?→ HW = min(ISR 所有副本 LEO),Consumer 只消费 HW 以下
[蚂蚁][R1] ISR 只剩 Leader 时写入会怎样?→ min.insync.replicas=2 时直接拒绝写入
[字节][R1] Kafka 副本机制和 Leader 选举过程?→ ISR 同步副本集合,Leader 掉线从 ISR 中选新 Leader
[蚂蚁][R1] Kafka ISR 机制如何工作?→ Follower 持续 Fetch,超时未追上则踢出 ISR,HW 由 ISR 最小 LEO 决定
[拼多多][R1] Kafka 怎么保证可靠性?→ acks=-1 + min.insync.replicas=2 + unclean.leader.election=false
idempotent-consumption
🎯 骨架
本质:MQ 普遍只能 at-least-once,重复是常态;幂等是消费端自己的责任,不是 MQ 保证的
三层防御:
L1 Redis SETNX(幂等 ID, TTL=业务最大窗口的 2-3 倍):拦截 99% 重复,业务失败要 del key 防误拦
L2 DB 唯一键兜底:业务单号/幂等 ID 建唯一索引,捕获 DuplicateKeyException 返回幂等结果
L3 业务状态机:处理前先查 status,已 completed 直接返回
方案对比:
Redis:高性能(μs 级)、弱一致(key 过期/Redis 宕机可能漏判),适合通知/日志类
DB 唯一键:强一致(事务保证)、性能中等,资金/权益必用
生产推荐双层:Redis 前置 + DB 兜底
幂等表 + 业务表同事务:幂等记录写入和业务操作必须在同一本地事务里,否则有窗口期
幂等 ID 由调用方生成:服务方生成无法识别"重试 vs 新请求";调用方传同一 UUID,服务方校验
写请求按场景选方案:
新增:DB 唯一键 + 幂等 Token
更新:乐观锁(version)或条件更新 WHERE status=old_status
删除:天然幂等
扣款/扣库存:原子条件更新 UPDATE SET amount=amount-? WHERE amount>=?,不需要锁
生产端也能幂等:enable.idempotence=true,Broker 用 PID + SequenceNumber 去重(仅单 Producer 单 Session)
⚠️ 易混淆
幂等 ≠ 不重复投递;幂等是允许重复但结果一致
Redis SETNX 成功后业务失败要 del key,否则合法重试被拦
key 过期时间设业务最大重试窗口的 2-3 倍,太短会误漏
纯 INSERT 不需要分布式锁(DB 唯一键原子);先查后写才需要锁
单条 GET 接口天然幂等,不要画蛇添足加幂等表
enable.idempotence 仅生产端单 Session,跨 Producer 重启需要事务消息
🎤 面经
[小红书]R2 #7296 如何确保你的消息只被消费一次?
答:at-least-once + 消费端幂等(Redis SETNX 前置 + DB 唯一键兜底);想严格 once 用事务消息+EOS
[蚂蚁]R1 #6669 Kafka 一致性原理;消费时的消息丢失和重复如何解决?
答:不丢:手动提交 offset、处理完再提交;不重复:幂等 ID + Redis/DB 去重
[B站]R1 #2758 项目中用的什么 MQ 消息队列?怎么保证消息不会被重复发送?
答:生产端 enable.idempotence=true(PID+SeqNum);消费端用幂等 ID + DB 唯一键兜底
[滴滴]R3 #2989 RocketMQ 用过吗?如何保障消息的幂等性?
答:业务唯一键作为幂等 ID;Redis SETNX 前置拦截 + DB 唯一索引 catch DuplicateKeyException 兜底
[蚂蚁]R1 #6625 什么是幂等锁?幂等和锁的区别?业务幂等与数据幂等的区别?
答:幂等锁=幂等校验工具;锁互斥并发,幂等保证多次结果一致;业务幂等看语义,数据幂等看主键唯一
[字节]R1 #4857 怎么解决网络波动导致的消息重复发送和重复消费的问题?
答:发送:enable.idempotence=true;消费:幂等 ID + Redis SETNX + DB 唯一键三层防御
[网易]R1 #1607 两条消息基本同时到消息队列中,判断逻辑该怎么做才能保证不重复消费?
答:业务单号做唯一索引 + 写库前 SELECT FOR UPDATE 或乐观锁;并发依赖 DB 串行化
[字节]R1 #453 商城项目中如何防止消息被重复消费?
答:订单号建幂等表唯一索引;Redis 标记已处理;消费幂等表写入和业务操作同事务
[阿里]R1 #88 消息队列事务消息的核心流程?怎么保证消息不丢失不重复?
答:半消息+本地事务+回查保证不丢;消费端幂等 ID + 唯一索引保证不重复
[蚂蚁]R1 #6641 MQ 怎么保证不重复消费、可靠性投递、可用性?
答:不重复=消费端幂等;可靠投递=生产端事务消息/本地消息表;可用=Broker 多副本+集群部署
offset-management
🎯 骨架
offset 是什么:消费者在 Partition 中消费到的位置,每个 Consumer Group 独立维护
存储位置:Kafka 0.9+ 存在内部 Topic __consumer_offsets(之前存 ZooKeeper)
提交方式:
自动提交(enable.auto.commit=true):定期提交,可能重复消费或丢消息
手动提交:commitSync(同步,阻塞)/ commitAsync(异步,不阻塞)
重复消费根因:① offset 未提交,重启后从旧 offset 重新消费;② Rebalance 触发(服务发布非优雅关闭,处理中消息 offset 未提交,Rebalance 后被其他消费者重新消费);③ 自动提交时序问题(定时提交,提交前宕机)
消息丢失根因:offset 已提交但业务处理失败,下次从新 offset 开始
⚠️ 易混淆
自动提交不是"处理完再提交",是定时提交,处理中宕机会丢消息
手动 commitSync 在 for 循环里每条提交性能差,建议批量提交
🎤 面经
[字节 1面] Kafka 如何保证不会重复消费?→ 手动提交 offset + 业务幂等(数据库唯一键/Redis 去重)
[Shopee 1面] 批量消费过长导致 rebalance?→ 增大 max.poll.interval.ms 或减少 max.poll.records
[美团 1面] Kafka 消费幂等性怎么保证?→ 消费端幂等:唯一键/Redis setNX/数据库乐观锁
[蚂蚁 3面] 为什么一个消费者对应一个 Partition?→ Kafka 设计:同一 Consumer Group 内一个 Partition 只能被一个消费者消费,保证有序性
[通用] offset 存在哪里?→ Kafka 0.9+ 存 __consumer_offsets 内部 Topic,之前存 ZooKeeper
[通用] 自动提交和手动提交的区别?→ 自动定时提交可能丢消息;手动提交精确控制,处理成功后再提交
transaction-message
🎯 骨架
要解决的核心问题:DB 写入和 MQ 发送的原子性
先写 DB 再发 MQ:DB 成功 MQ 失败 → 不一致
先发 MQ 再写 DB:MQ 成功 DB 失败 → 不一致
DB 和 MQ 同事务:MQ 不支持 XA → 不可行
方案一:本地消息表(Outbox Pattern):
业务表 + 消息表写在同一本地事务
事务提交后由独立 Relay 组件扫描 PENDING 消息发送 MQ
任何 MQ 都能用,秒级延迟,下游解耦最好
方案二:RocketMQ 事务消息(半消息机制)三阶段:
阶段 1:Producer → Broker 发半消息(Consumer 看不到)
阶段 2:Producer 执行本地事务(INSERT 订单等)
阶段 3:本地事务成功 → 发 COMMIT(消息变可见);失败 → ROLLBACK(消息删除)
事务回查兜底:
Producer 在阶段 2/3 之间宕机 → Broker 半消息一直 UNKNOWN
Broker 定时扫描未 COMMIT/ROLLBACK 的半消息,通过 Netty 长连接(自定义 Remoting 协议)回查 Producer
Producer 查 DB 确认状态 → 返回 COMMIT/ROLLBACK/UNKNOWN
默认回查 15 次,超过丢弃(业务层对账兜底)
TransactionListener 接口:
executeLocalTransaction(msg, arg):执行本地事务并返回状态
checkLocalTransaction(msg):Broker 回查时被调用
Outbox 改进 RocketMQ 事务消息缺点:TransactionListener 把业务和消息发送强耦合,下游多时主流程被切碎;Outbox 让主流程只写 DB,独立 Relay 投递
本地消息表 vs 事务消息选型:
本地消息表:MQ 不限、下游多、对延迟不敏感的场景
事务消息:只用 RocketMQ、对延迟敏感、不愿维护消息表
为什么不用 Seata XA:全局锁高并发性能差;运维复杂度高;秒级最终一致够用就 ROI 不划算
⚠️ 易混淆
事务消息 ≠ XA 事务;事务消息保证的是"DB 提交+MQ 投递"最终一致,不是 ACID
半消息存储在内部 Topic RMQ_SYS_TRANS_HALF_TOPIC,Consumer 看不到
回查协议是 RocketMQ 自定义 Remoting + Netty 长连接,不是 HTTP(HTTP 做不到服务端主动推)
Kafka 事务(KIP-98)和 RocketMQ 事务消息不是同一个东西:Kafka 事务是 Producer 跨 Topic 原子写,没有半消息+回查
本地消息表的 Relay 应该和业务事务解耦,不要塞在 TransactionListener 里
事务消息无法回滚已投递的消息,仍需消费端幂等
🎤 面经
[字节]R3 #331 讲讲 RocketMQ 事务消息的发送过程
答:半消息→本地事务→COMMIT/ROLLBACK;超时未提交触发 Broker 回查
[字节]R1 #450 RocketMQ 事务消息底层是怎么实现的?可靠吗?怎么才能可靠?
答:半消息+本地事务+回查;可靠靠回查兜底+对账系统
[Shopee]R1 #815 RocketMQ 事务消息:订单创建+发券如何保证最终一致性?(半消息+回查)
答:1 半消息→2 创建订单本地事务→3 COMMIT 消息变可见→4 发券消费者消费;回查兜底
[拼多多]R1 #608 RocketMQ 事务消息
答:三阶段:半消息→执行本地事务→提交/回滚;回查兜底;和本地消息表二选一
[阿里]R1 #88 消息队列事务消息的核心流程是什么?怎么保证消息不丢失不重复?
答:半消息+本地事务+回查保证不丢;消费端幂等保证不重复
[腾讯]R5 #7086 RocketMQ half message。介绍 half message, 失败如何回调
答:半消息=投递但不可见;执行本地事务返回 COMMIT/ROLLBACK;UNKNOWN 触发 Broker 回查 checkLocalTransaction
[快手]R1 #2633 不支持两段式提交怎么实现事务消息?
答:不用 XA:用半消息+本地事务+回查(最终一致),或本地消息表 Outbox Pattern
[腾讯]R1 #1650 本地消息表原理
答:业务+消息表同一本地事务;事务提交后异步扫描发送 MQ;失败重试,成功更新状态
[腾讯]R1 #1652 不用本地消息表,还可以用什么方式保证写 DB 和发 MQ?
答:RocketMQ 事务消息(半消息+回查);CDC 监听 binlog 投递(Debezium);TCC 补偿
[携程]R3 #1988 分布式事务相关(本地消息表 + 消息队列)
答:业务表+outbox 表同事务;Relay 扫描 outbox 投递 MQ;下游消费幂等
[拼多多]R1 #674 分布式事务上一家怎么处理的(MQ+本地消息表保证数据最终一致性)
答:Outbox 模式:业务+消息同事务;定时任务扫描;下游幂等消费;对账兜底
S3 性能极致
zero-copy-principle
🎯 骨架
传统 IO(read + write):
mmap + write:
sendfile:
sendfile + SG-DMA(DMA gather):
⚠️ 易混淆
mmap vs sendfile:拷贝次数相同(3 次),切换次数不同(4 vs 2)→ sendfile 更快
上下文切换最大代价:TLB 刷新 + Cache 污染(不是寄存器保存)
DMA:独立硬件芯片,CPU 发指令后自己搬数据,搬完中断通知 CPU
mmap 共享物理内存:通过页表让内核虚拟地址和用户虚拟地址指向同一物理页
Kafka:sendfile(消费只读转发,不修改数据,少 2 次切换);完全依赖 OS PageCache
RocketMQ:mmap(写消息需在用户空间构建 CommitLog 格式 + ConsumeQueue 索引);CommitLog 全局顺序写
RocketMQ 不能直接 sendfile:消息分散在 CommitLog 不同位置,先查 ConsumeQueue 再读 CommitLog,无法连续传输
Kafka 写:mmap;读:sendfile。RocketMQ 写读都靠 mmap
WAL 语义:redo log 必须先于数据页落盘,mmap 把刷盘控制权交给 OS,OS 不知道因果关系
后果:数据页可能先刷,崩溃时 redo log 没落盘,恢复失败
用 Buffer Pool + O_DIRECT 自己管缓存
🎤 面经
[京东]R1 #5499 零拷贝是什么?用来解决什么问题?有哪些应用场景?实现方式有哪些?
答:减少内核/用户态间无效拷贝;Kafka 文件传输;sendfile / mmap / DMA gather
[字节]R1 #374 零拷贝的概念是什么意思?
答:数据不经过用户态缓冲区,直接在内核态完成传输,减少 CPU 拷贝和上下文切换
[Shopee]R3 #890 zero copy 怎么实现的?
答:sendfile 系统调用:内核缓冲区→Socket 缓冲区,2 次切换;DMA gather 进一步降到 1 次 CPU 拷贝
[滴滴]R1 #2523 Kafka 的 log、index、稀疏索引;零拷贝、mmap、sendfile、DMA gather
答:Kafka 用 sendfile 读,mmap 写;mmap 映射 PageCache 可读写,sendfile 纯内核传输只读
[百度]R2 #6040 NIO 的原理,从操作系统说;零拷贝
答:NIO 基于 epoll/select 多路复用;零拷贝通过 FileChannel.transferTo 调用 sendfile
[拼多多]R4 #1098 网络编程 nio 和 netty 相关,netty 的线程模型,零拷贝实现
答:Netty 零拷贝:CompositeByteBuf 逻辑合并、FileRegion 调 sendfile、堆外内存避免 JVM 堆拷贝
[美团]R3 #1221 Kafka 怎么保证高吞吐量?零拷贝原理是什么?
答:顺序写 + sendfile 零拷贝 + 批量 + 完全依赖 PageCache;sendfile 把磁盘→网卡全程内核态完成
sequential-write
🎯 骨架
顺序写为什么快:磁盘顺序 IO 接近内存速度(HDD 顺序 100MB/s vs 随机 1MB/s,差 100 倍),SSD 上也减少写放大、延长寿命
Kafka 写入路径:Producer → Broker → 追加写 Log Segment 文件(mmap 映射 PageCache)→ OS pdflush 异步刷盘 → 多副本兜底可靠性
Kafka 默认异步刷盘:写 Page Cache 就返回,不等 fsync;靠 ISR 多副本保证不丢,不是单机同步刷盘
WAL 思想(Write-Ahead Log):先顺序写日志→后异步刷数据,跨产品通用:Kafka Log、MySQL redo log、RocksDB WAL、Redis AOF
Topic 多时退化:Kafka 每个 Partition 一个文件,>1000 时变成随机 IO,性能下降;RocketMQ 用 CommitLog 全局顺序写规避
Page Cache 的关键作用:写入直接进 PageCache(不阻塞 IO),消费命中率高时不读磁盘,进程重启不丢缓存(OS 没重启)
Kafka 高吞吐四要素:顺序写 + sendfile 零拷贝 + 批量(batch.size+linger.ms)+ 完全依赖 OS PageCache
⚠️ 易混淆
顺序写 ≠ 同步刷盘。Kafka 写到 PageCache 就返回(仍是顺序追加),fsync 是另一回事
Topic 数量 vs 分区数量:单 Broker Topic 多时(>1000)随机 IO,单 Topic 分区多时影响小
Kafka 顺序写在 Topic 少时优势最强,Topic 多时不如 RocketMQ CommitLog 稳定
WAL 不是只有数据库用:Kafka log、RocksDB WAL、Redis AOF 都是 WAL 思想
MySQL redo log:先顺序写 redo→后异步刷脏页,innodb_flush_log_at_trx_commit 控制 fsync 时机
RocksDB(LSM-Tree):写 WAL → MemTable → SSTable 顺序追加,写多读少场景王者
三者共同思想:把随机写变顺序写,用读放大换写性能
🎤 面经
[Shopee]R1 #2787 Kafka 为什么可以扛住这么高的 qps?
答:顺序写(PageCache 追加)+ sendfile 零拷贝 + 批量 + Partition 并行
[阿里]R2 #35 Kafka 高吞吐量原因?
答:顺序写日志 + 零拷贝 + 批量发送 + Page Cache 命中
[字节]R3 #948 Kafka 为什么吞吐量那么高?
答:顺序追加写 + sendfile 跳过用户态 + Producer 端 batch + Consumer 端 PageCache 命中
[美团]R3 #1221 Kafka 怎么保证高吞吐量的?设计模型,零拷贝?
答:顺序写 + 零拷贝 + 多分区并行;Producer 攒 batch;Broker 写 PageCache 不直接刷盘
[京东]R1 #4048 Kafka 为什么能做到高吞吐?
答:日志追加顺序写 + sendfile + batch + 异步刷盘靠多副本兜底
[滴滴]R1 #2523 Kafka 的 log、index、稀疏索引;零拷贝
答:Log 顺序追加;index 稀疏索引(每 4KB 建一条),减少索引体积;通过二分定位 Log 偏移
[滴滴]R3 #2995 commitLog 和 consume queue 分别用来干嘛?
答:CommitLog 全局顺序写消息体;ConsumeQueue 按 Topic+Queue 索引,存 offset+size+tag hash,约 20B/条
[小红书]R1 #5349 write ahead log
答:先写日志再改数据;崩溃恢复用日志重放;Kafka log / MySQL redo / RocksDB WAL 都是这个思想
partition-strategy
🎯 骨架
指定 partition → 直接发到该分区
指定 key → murmur2 哈希取模 → 固定分区(保证同 key 顺序)
无 key(Kafka 2.4+)→ Sticky Partitioner,同一 batch 发往同一分区,batch 满了再换(老版本才是纯轮询 Round Robin)
自定义 Partitioner → 实现 Partitioner 接口,按业务逻辑路由(城市/租户/VIP 优先级)
轮询(默认)→ 均匀分布到所有 MessageQueue
指定 key → 哈希取模 → 固定 Queue(保证同 key 顺序,如 orderId)
自定义 MessageQueueSelector → 实现接口,按业务逻辑路由
顺序消息(MessageListenerOrderly)→ 消费端加锁,同一 Queue 串行消费
⚠️ 易混淆
Kafka 2.4+ 无 key 默认是 Sticky Partitioner,不是纯轮询(高频错误)
同 key 同分区保证分区内有序,不是全局有序
扩 partition 后 key 哈希结果变化,历史顺序性被破坏
RocketMQ 顺序消息需要 Producer 端 MessageQueueSelector + Consumer 端 MessageListenerOrderly 配合
🎤 面经
[京东][R1] Kafka 的分区策略有哪些?默认分区策略和轮询策略的区别?
答:有 key 用哈希;无 key 2.4+ 用 Sticky(同 batch 同分区),老版本才是轮询
[字节][R1] 分区的目的是啥?创建 Topic 如何将分区放置到不同 Broker?
答:分区 = 并行度单位,消费者数上限;创建时 Kafka 按 Broker 数量均匀分配分区
[小红书][R1] 订单状态变更必须严格有序,如何设计分区策略?
答:orderId 为 key 哈希到固定分区,消费端单线程消费该分区
[美团][R1] MQ 消息如何保障顺序性?包括消息生产和消费两端
答:生产端:同 key 路由同分区;消费端:单线程消费 + 按 key 串行处理
[美团][R1] 流程引擎事件怎么保证顺序性?
答:key 哈希分区 + 消费端线程池按 key 路由到固定线程串行处理
[阿里][R1] Kafka 如何保障顺序消费?
答:同 key 路由同分区,每个分区只分配一个消费者
[京东][R4] 生产端无法保证消息顺序性时,如何保证消费者拿到的消息是有序的?
答:消费端按业务 key 路由到固定线程串行处理;或引入序列号在消费端重排序
producer-reliability
🎯 骨架
acks 三档:
acks=0:发了不管,最快最不可靠
acks=1(默认):Leader 写入即返回,Leader 切换前 Follower 没同步会丢
acks=-1/all:ISR 全部确认才返回,最安全
min.insync.replicas(Broker 端配套):
acks=-1 时 ISR 缩到只剩 Leader → 退化为 acks=1
必须配 min.insync.replicas≥2:ISR 不足直接拒写(NotEnoughReplicasException),宁可不可用也不丢
unclean.leader.election.enable=false:禁止非 ISR 副本当选 Leader,防止数据截断
retries + retry.backoff.ms:3-5 次重试 + 100ms backoff 避免重试风暴;retries=0 网络抖动也丢
enable.idempotence=true(生产端幂等):Broker 用 PID + SequenceNumber 去重,防重试导致重复
本地消息表(Outbox Pattern):业务操作和消息落表同一本地事务;定时扫描未发送/失败的重投;防止 Producer 进程崩溃丢内存消息
同步 vs 异步发送:核心业务用 send().get() 同步等结果或带回调;fire-and-forget 不要用,回调里要处理 onFailure
delivery.timeout.ms:发送总超时(默认 2 min),覆盖 retries 全过程;超时仍未成功视为失败
⚠️ 易混淆
acks=0/1/-1 中 -1 = all,0 是零确认(最弱),别搞反
acks=-1 单独不够:必须配 min.insync.replicas≥2,否则 ISR 缩水时退化成 acks=1
"Broker 不解压":压缩 batch 直接落盘,消费端解压(零拷贝优势)
异步刷盘 ≠ 不可靠:Kafka 默认异步刷盘,靠 ISR 多副本兜底
生产端幂等仅在单 Producer 单 Session 内生效,跨 Producer 重启需要事务消息
🎤 面经
[阿里]R2 #38 Kafka ack 设置?
答:0/1/-1 三档:0 不确认、1 Leader 确认、-1 ISR 全确认;可靠性升高、延迟升高
[Shopee]R1 #2786 Kafka 怎么保证不丢消息的?
答:生产端 acks=-1+重试+幂等;Broker min.insync.replicas≥2+unclean=false;消费端手动提交+幂等
[字节]R1 #7305 Kafka 消息丢失的场景有哪些?
答:acks=0/1 时 Leader 切换;异步刷盘 Broker 宕机;自动提交 offset 后处理失败;retries=0 网络抖动
[字节]R1 #7318 Kafka 消息丢失的场景有哪些?如何保证消息不丢失?
答:三端可靠性:acks=-1 + ISR≥2 + unclean=false + 手动提交 + 幂等消费 + 对账兜底
[阿里]R4 #147 Kafka 怎么保证消息不丢、重复发了怎么办?
答:不丢靠 acks=-1+ISR;重复靠 enable.idempotence=true(PID+SequenceNumber 去重)+ 消费端幂等
[字节]R5 #295 项目中如何解决重复消息和保证消息不丢?
答:不丢:acks=-1+本地消息表 Outbox 兜底;不重复:生产端 idempotence + 消费端唯一键去重
[拼多多]R3 #1250 Kafka 的原理?怎么保证消息不丢失?
答:写 PageCache + 多副本 ISR 同步;生产 acks=-1,Broker min.insync.replicas≥2,消费手动提交
[字节]R3 #949 Kafka 如何保证 Exactly once?Producer 一个消息没有发送成功怎么办?
答:幂等 Producer + 事务(producer.initTransactions / sendOffsetsToTxn)= EOS;发送失败由 retries 自动重试,超 delivery.timeout.ms 才视为失败
[字节]R1 #6185 Kafka 生产者发了错误的消息怎么办?
答:MQ 不支持回滚,依赖消费端幂等 + 业务补偿(DLQ 兜底 / 反向消息冲销)
S4 消费治理与选型
ordered-consumption
🎯 骨架
Kafka 顺序保证粒度:分区内有序,跨分区无序;全局有序需单 Partition(吞吐暴跌,几乎不用)
三个会乱序的环节:
生产端:多线程发送、重试导致后发先到(max.in.flight.requests.per.connection > 1 + retries 时)
Broker:多 Partition 天然无序
消费端:多线程消费、Rebalance 重新分配
生产端保序:
业务 key(如 orderId)哈希到固定 Partition
重要场景设 max.in.flight.requests.per.connection=1 防重试导致的乱序(开 idempotence 后可放宽到 5)
消费端保序两种模式:
单线程消费分区:最简单,吞吐受限
线程池按 key 路由到固定线程:吞吐高,每个线程内串行处理同 key(推荐)
RocketMQ 顺序消息:Producer 端 MessageQueueSelector 把同 key 路由到固定 MessageQueue + Consumer 端 MessageListenerOrderly(消费端加锁,同 Queue 串行)
顺序消费三大痛点:
异常阻塞:一条失败后续全堵 → 重试队列 + 死信队列兜底,不阻塞主流程
全局有序难:跨 Partition 无法保证 → 业务接受局部有序
Rebalance 打断:分区重新分配可能导致重复或乱序 → CooperativeSticky + 优雅停机
扩 Partition 后顺序破坏:原来同 key 哈希到 P0,扩容后哈希到 P3 → 历史顺序断裂;解决:扩容前先排干历史,或新老 key 双写过渡
⚠️ 易混淆
同 key 同分区是分区内有序,不是全局有序
单线程消费 ≠ 单 Consumer,是同一 Partition 由一个 Consumer 内单线程处理
多线程消费保序必须按 key 路由到固定线程,不能简单线程池 submit
RocketMQ 顺序消费靠 Consumer 端加锁,性能比 Kafka 分区有序低
enable.idempotence=true 时 max.in.flight 可以是 5 仍然有序(Broker 端按 SequenceNumber 重排)
🎤 面经
[阿里]R2 #33 Kafka 顺序消费?多消费者情况下如何保障顺序?
答:同 key 哈希同分区;每个分区只分配一个消费者;消费内单线程或按 key 串行
[蚂蚁]R4 #3428 MQ 如何做到顺序消费?消费失败的场景如何处理?
答:同 key 路由 + 单线程消费;失败进重试队列+死信队列,不阻塞主流程
[携程]R1 #2890 MQ 的顺序消费痛点?
答:异常后阻塞;多队列分布式全局难;扩缩容触发 Rebalance 打断顺序
[字节]R1 #570 消息队列消息顺序执行这些
答:生产端按 key 哈希分区;消费端按 key 路由到固定线程串行处理
[字节]R1 #382 用过 MQ 嘛?如何保证有序?消息丢失?
答:顺序:同 key 同分区+单线程消费;不丢:acks=-1+ISR≥2+手动提交+幂等
[字节]R1 #969 Kafka 发送消息详细过程?重点说一下保证消息有序性、和可靠性?
答:序列化→分区器(同 key 哈希)→Accumulator→Sender;保序需 max.in.flight.requests=1 或开幂等
[携程]R1 #7252 MQ 消息有序性会在哪些阶段出现问题?
答:生产端重试乱序、多 Partition 无序、消费端多线程、Rebalance 打断
[Shopee]R1 #919 RabbitMQ 能保证消息的有序性吗?
答:单 Queue 单 Consumer 单线程才能保证;多消费者并发就乱
[拼多多]R3 #6913 RabbitMQ 如何保证消息的顺序执行?
答:单 Queue + 单 Consumer + 手动 ACK,避免并发;和 Kafka 单分区思路类似
[B站]R3 #3318 RocketMQ 消费者重平衡会有什么问题?重复消费?消费失败?
答:STW 期间消费暂停;offset 未提交的消息被新 Consumer 重新消费;顺序场景下乱序
rebalance-mechanism
🎯 骨架
是什么:Consumer Group 成员变化时重新分配 Partition 的过程
6 个触发条件:
Consumer 加入/离开(扩缩容/宕机)
心跳超时(session.timeout.ms 默认 10s)
处理超时(max.poll.interval.ms 默认 5min)
Topic 的 Partition 数变化
订阅 Topic 列表变化
GroupCoordinator 切换(其所在 Broker 故障)
三大问题:
STW(Eager 模式):所有 Consumer 暂停消费
重复消费:offset 未提交的消息被重新分配后重消费
消费延迟:Rebalance 期间消息堆积
分配策略对比:
RangeAssignor(默认):按 Topic 分配,不均匀,Eager
RoundRobinAssignor:轮询全局,均匀,Eager
StickyAssignor:尽量保持原分配,仍是 Eager
CooperativeStickyAssignor(2.4+):增量 Rebalance,只迁移必要的 Partition,无 STW
Cooperative 增量流程:
触发 Rebalance → 只放弃"需要转移"的 Partition,其他继续消费
分多轮完成调整,每轮只迁移一小部分
不再"全停→全分配→全恢复",而是"边消费边调整"
GroupCoordinator:
每个 Group 一个,所在 Broker = hash(group.id) % __consumer_offsets 分区数
负责心跳、Rebalance 协调、offset 提交
消费组内选 Leader Consumer 计算分配方案
减少 Rebalance 影响 4 招:
session.timeout.ms=25-30s、heartbeat.interval.ms=session/3
max.poll.interval.ms 根据业务实际处理时长设置
用 CooperativeStickyAssignor(需手动配,不是默认)
优雅停机:consumer.wakeup() + finally 块 commitSync() + close()
Eager → Cooperative 升级:先并存 CooperativeStickyAssignor,RangeAssignor → 滚动重启 → 移除 RangeAssignor
⚠️ 易混淆
CooperativeStickyAssignor 不是默认值,必须显式配置才能启用
session.timeout.ms(心跳超时)和 max.poll.interval.ms(处理超时)是两个独立超时
sessionTimeout 触发 = Consumer 心跳没发就被踢
pollTimeout 触发 = 两次 poll 间隔太长(业务慢)就被踢
心跳是 Coordinator 探活 Consumer,和 ISR 踢出(Broker 间副本同步)不是一回事
Rebalance 期间整个消费组暂停(Eager),不只是单个 Consumer
重启前 commitSync + close 才能避免重复消费,不优雅停机靠 timeout 兜底会重消费
升级 CooperativeSticky 必须分阶段,混用协议会异常
🎤 面经
[字节]R1 #7319 Kafka 消费者组 Rebalance 什么时候触发?如何减少 Rebalance?
答:加入/离开/心跳超时/poll 超时/Partition 变化;调大超时参数+CooperativeSticky+优雅停机
[拼多多]R1 #862 Kafka 的 rebalance
答:触发条件 6 个;STW 期间消费暂停;CooperativeSticky 增量减少 STW
[滴滴]R1 #2524 Kafka rebalance 策略,具体过程。GroupCoordinator 选举、消费组协调器选举过程?
答:4 种 Assignor;Coordinator = hash(group.id) % __consumer_offsets 分区对应的 Leader Broker
[Shopee]R3 #895 Kafka 批量消费,如果消费过长,会导致消费超时,触发 rebalance 吧?sessionTimeout 和 pollTimeout 有什么区别,哪个会触发 rebalance?
答:都会触发;sessionTimeout 是心跳超时,pollTimeout 是两次 poll 间隔超时(业务慢的元凶)
[腾讯]R1 #1653 Kafka rebalance,消息堆积如何处理?
答:两件事:减少 Rebalance(CooperativeSticky+调大超时)+ 处理积压(线程池并行+扩容)
[阿里]R2 #37 Kafka 重平衡过程,会造成什么问题,是否可对外服务?
答:STW 暂停消费;offset 未提交导致重复;Eager 模式不可对外,Cooperative 部分可服务
[美团]R3 #1221 Kafka 重平衡,重启服务怎么保证 kafka 不发生重平衡?
答:优雅停机+静态成员(group.instance.id),重启时 Coordinator 不立即 Rebalance 等成员回来
[other]R1 #4488 重平衡会带来什么问题?stw、消息堆积、重复消费、Kafka 的渐进式重平衡知道吗?
答:STW + 重复消费 + 消费延迟;渐进式 = CooperativeSticky 增量 Rebalance,只迁移必要 Partition
[other]R1 #4367 Kafka 的重平衡问题介绍下?重平衡的过程是怎么样?
答:触发 → JoinGroup → SyncGroup → 完成;STW 期间暂停;问题:STW、重复、延迟
[B站]R3 #3318 RocketMQ 消费者重平衡会有什么问题?重复消费?消费失败?
答:STW 期间暂停;消费失败用重试队列+DLQ;重复用幂等
mq-comparison
🎯 骨架
三者定位:
Kafka:大数据流处理 / 日志收集 / 高吞吐场景(百万级 TPS)
RocketMQ:业务消息(电商/金融),事务/延时/顺序消息原生支持
RabbitMQ:企业级消息,低延迟(μs 级)+ 复杂路由(exchange + binding)
核心维度对比:
存储模型核心差异:
Kafka:每个 Partition 一个独立文件,Topic 多时退化随机 IO;写读都依赖 OS PageCache,读用 sendfile
RocketMQ:所有消息写 CommitLog(全局顺序),ConsumeQueue 只存索引(offset+size+tagHash 共 20B);Topic 多仍稳定,但读多一次 ConsumeQueue 查询,不能直接 sendfile(用 mmap)
RabbitMQ:Erlang VM 进程内消息(mnesia/磁盘队列),单 Queue 性能受单机限制
为什么 Kafka 比 RocketMQ 快:
完全依赖 PageCache(不走用户态)
sendfile 零拷贝(少 2 次切换,比 mmap 快)
顺序追加 + 批量 + 压缩
但 Topic 多时优势消失,RocketMQ CommitLog 此时更稳定
选型决策树:
大数据流处理/日志收集 → Kafka
电商/金融事务+延时+顺序 → RocketMQ
企业级低延迟+复杂路由 → RabbitMQ
不确定:业务消息选 RocketMQ,日志/数据流选 Kafka
小哈换电场景:
账单结算消息:RocketMQ(事务消息保证一致性)
用户行为日志:Kafka(高吞吐+大数据分析)
实时通知:RabbitMQ(低延迟)
选型五看:吞吐要求、延迟要求、消息特性(事务/延时/顺序)、运维成本、生态匹配
⚠️ 易混淆
"Kafka 吞吐高"是 Topic 少时;Topic > 1000 时随机 IO,吞吐反不如 RocketMQ
"Kafka 不能做业务消息"是误区,能用,只是事务/延时要自己造轮子
顺序消费两者粒度都是分区/队列,全局有序都不实用
RabbitMQ μs 级延迟靠 Erlang 调度+消息体小+不持久化默认,持久化后延迟也是 ms 级
KRaft 是 Kafka 2.8+ 替代 ZK 的方案,3.3 GA,简化运维
🎤 面经
[腾讯]R2 #3115 Kafka 和 RocketMQ 的区别
答:存储:Kafka 多文件 vs RocketMQ CommitLog;功能:Kafka 简、RocketMQ 全(事务/延时/顺序);选型按业务特性
[滴滴]R3 #2991 说说 ActiveMQ, RabbitMQ, RocketMQ, Kafka 各种 MQ 之间的对比?
答:吞吐:Kafka>RocketMQ>RabbitMQ>ActiveMQ;延迟反向;功能:RocketMQ 最全
[Shopee]R1 #2784 kafka 与 rabbitmq 区别
答:Kafka 高吞吐+顺序追加+ms 级;RabbitMQ AMQP+复杂路由+μs 级,吞吐低
[京东]R1 #5467 项目中用到的消息队列的消息模型是怎么样的?其他主流的 kafka、rabbitMQ 有啥区别?
答:模型:发布订阅 vs 点对点;Kafka 拉模式高吞吐,RabbitMQ 推+exchange 路由灵活
[美团]R1 #4921 rabbitmq 和 kafka 的区别
答:设计哲学:RabbitMQ 通用消息中间件,Kafka 流处理日志系统;吞吐和延迟是反向的
[阿里]R5 #215 消息队列技术选型。对比常见的 RabbitMQ、RocketMQ 和 Kafka 技术特点
答:Kafka 大数据;RocketMQ 业务消息;RabbitMQ 企业级;按吞吐/延迟/特性五看选型
[字节]R1 #403 项目里用了 Kafka,那聊一下 RocketMQ 和 Kafka 的区别
答:同上;侧重存储模型(多文件 vs CommitLog)和功能丰富度(Kafka 简单 vs RocketMQ 业务功能多)
[字节]R2 #625 项目里用了 Kafka,那聊一下 RocketMQ 和 Kafka 的区别
答:多 Topic 场景 RocketMQ 更稳;事务+延时 RocketMQ 原生;Kafka 大数据生态强
[蚂蚁]R2 #4121 为什么说 kafka 比 rocketmq 快
答:Kafka 用 sendfile(少切换)+ 完全依赖 PageCache + 多文件;RocketMQ mmap+CommitLog+多一次 ConsumeQueue 查询
[蚂蚁]R1 #4164 因为项目中既用了 Rocketmq 也用了 kafka,所以问了两者有何区别
答:业务消息走 RocketMQ(事务/延时/顺序);日志/数据流走 Kafka(高吞吐)
[蚂蚁]R4 #3427 系统中使用 MQ 需要考虑的点都有哪些?如果做好 MQ 的技术选型?
答:五看:吞吐/延迟/消息特性/运维成本/生态;考虑可靠性/顺序/重复/积压/扩缩容
[百度]R2 #7162 用的什么 MQ 消息队列?从哪些角度技术选型?
答:业务匹配(事务/延时)+ 性能(吞吐/延迟)+ 可靠性 + 运维成本 + 团队熟悉度
[蚂蚁]R1 #4209 常见的消息队列及其区别,什么场景下用什么 mq
答:Kafka 流处理;RocketMQ 业务消息(电商/金融);RabbitMQ 复杂路由+低延迟;按场景选
S5 生产实战
message-backlog
🎯 骨架
本质:生产速度 > 消费速度,consumer lag 持续增长
三大根因:
消费能力不足(慢接口、复杂计算、下游超时)
消费者数 > Partition 数(多余消费者空转)
突发流量(大促/秒杀生产瞬时飙升)
应急 SOP 优先级(最快 → 最慢):
① 消费者内部线程池并行(最快,配置中心动态调,秒级生效,不发版)
② 扩容消费者实例(前提:Partition 有富余)
③ 检查并修复下游依赖(DB/Redis/RPC 超时)
④ 扩 Partition + 扩消费者(大厂需审批,30min-2h)
⑤ 临时转储(消费出来写快速存储,后续慢慢处理)
线程池并行三个注意点:
顺序性:按 key 路由到固定线程,同 key 串行处理
offset 提交:等所有线程处理完再提交(CountDownLatch)
资源管控:有界队列防 OOM,拒绝策略要重试(不能丢)
事前预防:
容量规划:Partition 数 ≥ 预期最大消费者数
压测:确认单条处理耗时和峰值 QPS
监控:consumer lag 阈值告警、生产/消费速率监控
事后复盘:
量化影响:积压量、影响时长、用户/订单数
根因分类:流量突增 / 消费慢 / 下游故障 / 配置错误
改进项:扩容预案、监控阈值调整、限流降级
反模式:堆积只加消费者不看 Partition 数 → Rebalance 风暴 + 多余 Consumer 空转,积压纹丝不动
⚠️ 易混淆
消费者数 > Partition 数:多余消费者空转,不会分到 Partition
"扩 Partition" 是正确长期方案但不是最快应急手段;面试官追问"线上正在堆积第一时间做什么"答线程池并行
扩 Partition 后老 key 哈希结果变化 → 顺序性破坏,扩容前要排干历史
消息积压 ≠ 消息丢失;积压是消息堆在 Broker,丢失是 Broker 上没有
Broker 性能问题导致积压较少见,磁盘故障一般表现为发送失败而非积压陡增
高危操作:消费点位重置;点位被重置到历史位置会导致大量历史消息重复消费
🎤 面经
[拼多多]R1 #876 用 Kafka 吗?遇到消息堆积过?消息堆积怎么处理?加线程还是调整消费者?
答:第一时间:消费端线程池并行(按 key 路由);其次扩 Partition+扩消费者;治本要修慢消费/下游
[字节]R3 #328 消息堆积解决方案
答:紧急:线程池并行+扩容;中期:扩 Partition;长期:限流+容量规划+下游性能优化
[腾讯]R1 #1653 Kafka rebalance,消息堆积如何处理
答:Rebalance 期间消费暂停加剧积压;治理用 CooperativeSticky;积压用线程池并行+扩容
[美团]R1 #1309 消息堆积应该怎么处理?针对事前、事中、事后分别有什么应对方案?
答:事前:容量规划+压测+监控;事中:线程池并行+扩容+降级;事后:复盘+改进监控
[小红书]R2 #7297 MQ 消息队列消息堆积问题排查和解决思路?
答:看 lag 趋势→定位是生产暴涨还是消费变慢→对症处理:扩消费者/优化消费/扩 Partition
[腾讯]R1 #5060 MQ 消息积压如何处理?
答:三段式:事前容量+监控;事中线程池+扩容+降级;事后复盘+改进
[网易]R1 #1583 Kafka 消息积压如何排查和解决?
答:看 consumer lag→排查 GC/慢接口/下游→线程池并行处理;扩 Partition 是兜底
[滴滴]R3 #2572 Kafka 怎么排查积压?
答:kafka-consumer-groups 看 lag;定位慢消费的 Partition;排查消费逻辑/下游/GC
[快手]R1 #2622 MQ 有消息积压吗
答:大促有过,最严重一次 5400 万;事中线程池并行+扩消费者;事后扩 Partition+限流
[携程]R1 #7274 MQ 遇到积压怎么去分析和解决?
答:看监控(lag、生产消费速率)→ 定位根因(消费慢/下游故障/扩容)→ 应急(线程池+降级)→ 复盘
[得物]R1 #2696 当消息过多,也就是出现消息堆积时如何处理?
答:紧急:消费端并行+扩容;分析:是消费慢还是生产爆;治本:消费逻辑优化+下游兜底
[京东]R2 #3400 生产上遇到消息挤压怎么做?
答:第一时间扩并发(线程池);同步扩 Partition+消费者;查下游慢的根因;DLQ 兜底失败
delay-message
🎯 骨架
典型场景:订单 30min 超时关闭、券到期提醒、支付状态轮询、IM 消息撤回延时
三种主流实现:
RocketMQ 4.x:18 个固定延迟级别(1s/5s/10s/.../2h),消息先存 SCHEDULE_TOPIC_XXXX,定时任务扫描到期投递
RocketMQ 5.x:TimerWheel(时间轮)+ RocksDB 持久化,秒级精度,任意延迟时长
Kafka + 外部存储:Kafka 不原生支持,消息先写 Redis ZSet/DB,定时扫描到期投递目标 Topic
TimerWheel + RocksDB 详解:
时间轮 = 内存环形数组,每槽 1 秒;指针每秒走一格,到期取出投递
长延迟用 round 计数器,转完一圈 round-1,减到 0 才投递
短延迟(如 < 几小时):放时间轮 + 写 RocksDB
长延迟(如 7 天):只写 RocksDB,后台扫描快到期的加载到时间轮
重启后从 RocksDB 重建时间轮
RocksDB 选型理由:LSM-Tree(顺序追加写、写多读少)+ 按 Key 范围扫描(Key=投递时间戳+消息ID)+ 持久化兜底
Kafka 不原生支持的原因:Kafka 设计哲学是日志追加(按 offset 顺序存储),延迟消息要"到期才可见",与顺序追加模型冲突
Redis ZSet 实现痛点:score=投递时间戳;定时 ZRANGEBYSCORE 扫到期;问题:单 key 大、扫描有精度损失、Redis 宕机丢消息(需 RDB+AOF 兜底)
延时消息 ≠ 延时队列:延时消息是 MQ 内置功能,延时队列是用户自己用 ZSet/DB+定时器实现
⚠️ 易混淆
RocketMQ 4.x 不是任意延迟,只有 18 个固定级别;想 7 分钟只能选 5 或 10 分钟
时间轮不是分布式的,单 Broker 内的内存结构;分布式延时靠 RocksDB 持久化重建
RocksDB ≠ Redis;前者是 LSM-Tree 嵌入式存储,后者是内存 KV
Redis ZSet 实现延时队列:精度差(依赖扫描频率)、单 key 倾斜、过期清理麻烦
延时消息支持事务:RocketMQ 5.x 支持,4.x 因为是延后投递不支持半消息组合
🎤 面经
[字节]R1 #978 RocketMQ 用在什么地方?5.x 新版本延时队列做了什么优化?如果不用时间轮,你怎么设计一个延时队列?
答:5.x 用 TimerWheel + RocksDB 突破 18 级;不用时间轮可用 Redis ZSet score=触发时间 + 定时 ZRANGEBYSCORE 扫描
[字节]R1 #512 RocketMQ 延时消息(项目用到了)底层怎么实现的?消息量太大导致读消息延迟时间很长怎么办?
答:4.x 18 级 SCHEDULE_TOPIC_XXXX;5.x TimerWheel+RocksDB;量大时分片+并发扫描+延迟阈值降级
[滴滴]R3 #2994 RocketMQ 消息类型?延时性消息支持事务吗?
答:普通/顺序/延时/事务/广播/批量;4.x 延时不支持事务(半消息无法延后投递),5.x 支持
[B站]R1 #5839 RocketMQ 延时队列的底层?
答:18 级延时 → 投到对应延时 Topic 队列 → ScheduleMessageService 定时扫描到期消息恢复原 Topic 投递
[腾讯]R1 #5025 什么是时间轮算法?它在 Netty 和 RocketMQ 中有什么应用?
答:环形数组+指针前进,O(1) 添加/删除任务;Netty HashedWheelTimer 处理超时,RocketMQ 5.x 时间轮处理延时
[字节]R1 #979 Redisson 的时间轮了解过吗?说一下原理吧
答:单层时间轮+round 计数;HashedWheelTimer 默认 100ms tick,支持百万级定时任务
[美团]R1 #4789 RocketMQ 延迟队列怎么做的?Kafka 能实现延迟队列吗?
答:RocketMQ 4.x 18 级;Kafka 不原生,需外部存储(Redis ZSet/DB+定时投递)
[腾讯]R2 #7139 redis 实现延迟队列
答:ZSet score=投递时间戳;定时 ZRANGEBYSCORE 0 now 扫到期;缺点:单 key 大、精度依赖扫描频率
[拼多多]R1 #611 场景题:券过期的问题
答:推荐 RocketMQ 延时消息;备选 Redis ZSet+定时器;扫表方案不可取(数据量大延迟高)
[字节]R3 #7119 延时消息队列怎么设计?Redis 的 zset 做延时队列会有什么问题?
答:score=触发时间 ZRANGEBYSCORE 扫描;问题:精度(扫描频率)、单 key 大、Redis 宕机丢消息、扫描+POP 非原子
[other]R1 #4473 定时关单如何实现的?rocketmq 延迟队列?这个方案有缺点吗?
答:RocketMQ 延时消息最稳;缺点:4.x 固定 18 级、Broker 单点压力、消息体大时占空间
dead-letter-queue
🎯 骨架
本质:消费失败的"毒消息"兜底机制,防止一条坏消息阻塞整个队列
RocketMQ 机制:
消费失败自动重试,默认 16 次
间隔递增:1s → 5s → 10s → 30s → 1m → 2m → 3m → ... → 30m
重试消息进入 %RETRY%ConsumerGroup 重试队列
16 次全失败 → 进入 %DLQ%ConsumerGroup 死信队列
死信不再自动消费,需人工介入
Kafka 没有原生 DLQ:
自建 DLQ Topic:消费失败 → 发到 xxx.DLT Topic → 单独 Consumer 处理/告警
Spring Kafka:DeadLetterPublishingRecoverer 封装
消息体里要带原始 Topic、失败原因、重试次数、时间戳
DLQ 必备四件套:
监控告警:DLQ 消息量阈值告警,不能默默堆积
上下文保留:失败原因/堆栈/原始消息体
管理后台:查看、手动重投、批量处理
重试策略区分:可重试(下游超时)vs 不可重试(数据格式错误,直接 DLQ)
重试次数权衡:
多(16 次):恢复机会大但阻塞时间长
少(3-5 次):快速进 DLQ 但临时故障可能误杀
一般推荐:可重试错误重 16 次,不可重试 1 次直接 DLQ
DLQ 消息处理流程:
监控告警触发 → SRE/研发查看
分析失败原因 → 修复代码或下游
手动重投或批量处理 → 验证消费成功
DLQ 反模式:DLQ 不监控(消息默默堆积,业务数据丢失无人知)
⚠️ 易混淆
死信队列 ≠ 重试队列;先重试后死信,重试用 RETRY topic、最终失败才进 DLQ
RocketMQ DLQ 是 %DLQ%ConsumerGroup,每个 ConsumerGroup 独立的 DLQ
Kafka 没有内置 DLQ,靠 Spring Kafka 或自己实现
顺序消费场景用 DLQ 要小心:失败消息进 DLQ 后顺序就破了,必须业务接受局部跳过
DLQ 消息处理也要幂等,重投可能重复
🎤 面经
[京东]R1 #5916 死信队列有了解吗?什么情况下消息会变成死信?
答:RocketMQ 消费失败重试 16 次仍失败 → 进 %DLQ%ConsumerGroup;RabbitMQ:消息被拒绝/TTL 过期/队列满
[美团]R1 #1308 消息进入死信队列后,应该怎么去感知到和处理?
答:监控告警阈值;管理后台查看上下文;分析根因后手动重投或批量补偿
[美团]R4 #1331 Kafka 死信队列、消息顺序性
答:Kafka 无原生 DLQ,自建 DLT topic;顺序场景 DLQ 会破顺序,需业务接受局部跳过
[other]R1 #4378 死信队列用过吗?死信队列的消息需要走 exchange 吗?死信队列可以做什么?延迟消息
答:RabbitMQ DLX:原队列 TTL 过期/被拒/队满 → 通过 dead-letter-exchange 路由到 DLQ;DLX+TTL 可实现延时消息
[B站]R1 #5839 RocketMQ 延时队列的底层?死信问题?
答:死信:重试 16 次仍失败进 %DLQ%;处理:告警+管理后台+手动重投
[蚂蚁]R4 #3428 MQ 如何做到顺序消费?消费失败的场景如何处理?
答:顺序:同 key 同分区+单线程;失败:重试队列+DLQ 兜底,避免阻塞主流程
[阿里]R5 #217 RocketMQ 消费失败如何解决?n+1 问题?
答:重试队列+DLQ;n+1 问题(一条阻塞后续 n 条)→ 业务接受局部跳过+DLQ 异步重投
[B站]R3 #3318 RocketMQ 消费者重平衡会有什么问题?消费失败如何处理?
答:重复消费用幂等;失败用重试队列+DLQ;Rebalance 期间消费暂停靠 CooperativeSticky 减少 STW
[other]R1 #4485 Kafka 如果消费失败了怎么办?
答:业务异常进 DLT topic + 告警;下游故障启动短暂熔断;幂等消费保证重投安全
reliability-troubleshoot
🎯 骨架
核心 SOP 4 步:① 建战事群通报(1min) → ② 区分真假丢失(5-10min) → ③ 止血(10-30min) → ④ 三端排查定位根因(30min-2h)
真丢失 vs 假丢失(最常见的是假丢失):
真丢失:消息未写入 Broker 磁盘(acks 配错、网络分区、Broker 全挂)
假丢失:消息已持久化,消费端没读到(Rebalance 触发 offset 重置、消费者崩溃、过滤逻辑 Bug、订阅错 Topic)
判断方法:消息轨迹平台 / kafka-dump-log 离线确认(禁止 kafka-console-consumer --from-beginning,会打满带宽)
三端定位 checklist:
生产端:发送日志确认成功?回调 onFailure?delivery.timeout.ms?是否触发 Quota 限流?
Broker 端:ISR 缩水?磁盘满?retention 过期清理?GC 停顿导致 Leader 切换?网络分区/脑裂?UnderReplicatedPartitions、ActiveControllerCount(必须=1)、OfflineLogDirectoryCount、RequestHandlerAvgIdlePercent
消费端:offset 跳跃(Rebalance)?自动提交跳过?反序列化失败被静默丢?GC 停顿心跳超时被踢?
事前预防三件套:
配置加固:生产端 acks=-1+幂等+回调;Broker min.insync.replicas≥2+磁盘水位监控;消费端手动提交+幂等+DLQ
可观测性:Prometheus 监控 lag/速率/ISR;链路追踪 traceId 全程可追溯
对账系统(大厂标配):生产/消费各记流水,离线 Job 定时比对差异告警;这是发现"少量缓慢丢失"的唯一可靠手段(监控 lag 发现不了)
止血优先于定位:
黄金 1min:消费降级开关(跳过核心处理只记日志)
5min:消费者下线或流量调度
Broker 问题:先限流→灰度切流到备用集群→观察指标(不能直接全切,备用扛不住)
生产端问题:本地内存队列+异步批量发送(不直接写 DB,会退化成同步调用)
修复+补发:
从对账系统定位丢失消息 ID
小流量验证补发逻辑和去重能力
准备回滚方案
审批后执行(涉及资金/订单的核心数据需要审批+灰度+验证)
事后复盘必做项:根因报告、补告警、对账系统覆盖检查、容量评估、定期演练
⚠️ 易混淆
大部分"消息丢失"是假丢失,不要先怀疑 Broker
真丢失最常见的是 retention 过期清理(消费慢→堆积→磁盘满→retention 强制清理)
异步刷盘 + 单副本时 Broker 宕机会丢;多副本兜底就 ok
配置都到位仍可能丢的场景:① ISR 全部掉线+unclean election ② 异步刷盘 Broker 宕机 ③ retries 耗尽+本地消息表延迟 ④ 持续消费失败死信无人处理
「Producer 发送成功」≠「Consumer 可消费」:Consumer 看 HW=min(ISR LEO),Leader 写完不等于 HW 推进
P6:能说出"三端排查"
P7:完整时间线(建群通报→真假区分→止血→定位→修复)+ 对账系统意识 + 补发审批流程
🎤 面经
[字节]R1 #7305 Kafka 消息丢失的场景有哪些?
答:生产 acks=0/1+Leader 切换;Broker 异步刷盘+宕机;消费自动提交+处理失败;retries=0+网络抖动
[字节]R1 #7318 Kafka 消息丢失的场景有哪些?如何保证消息不丢失?
答:三端:acks=-1+ISR≥2+unclean=false+幂等+手动提交+对账兜底
[蚂蚁]R1 #6669 Kafka 一致性原理;消费时的消息丢失和重复如何解决?
答:消费手动提交+幂等;丢失靠对账系统兜底定位+小流量补发
[百度]R2 #7180 MQ 重复消费、丢消息的问题怎么解决?
答:重复=消费端幂等;丢消息=三端可靠性配置+对账系统兜底
[蚂蚁]R1 #5777 消息队列怎么解决消息丢失和顺序性?
答:不丢:三端配置+本地消息表 Outbox;顺序:同 key 同分区+单线程消费
[百度]R1 #3422 RocketMQ 如何防止消息丢失?
答:同步发送+同步刷盘(SYNC_FLUSH)+同步双写(SYNC_MASTER)+消费手动 ACK+幂等
[字节]R1 #382 用过 MQ 嘛?如何保证有序?消息丢失?
答:不丢三端配置;顺序同 key 同分区+单线程消费
[other]R1 #4515 其他线上问题排查过吗?OOM 问题、消息堆积。如果现在线上出现了 OOM,你会如何排查?
答:MQ 排障:先看监控(lag、ISR、速率)→ 真假丢失区分 → 三端定位 → 对账兜底
[百度]R1 #2441 项目挖掘:任务系统监听 MQ,RocketMQ 默认 20 个线程导致 CPU 飙高,是否可能造成消息丢失?
答:拒绝策略丢消息(AbortPolicy)会丢;用 CallerRunsPolicy 或自定义 sleep 重试;改有界队列+合理拒绝
[拼多多]R1 #3407 缓存一致性?cannal binlog mq 具体过程?怎么防止重复消费?怎么防止消息丢失?
答:CDC + MQ:本地消息表/binlog 兜底;消费端幂等+对账;丢失走三端排查 SOP
S* 通用
⚠️ producer-retry (待补)
ELASTICSEARCH
S1 ES基础原理
es-inverted-index
🎯 骨架
★ 倒排索引 = 关键词 → 文档 ID 列表(vs 正排:文档 ID → 内容);为全文搜索而设计
★ 三层结构:Term Index(FST,常驻内存)→ Term Dictionary(磁盘,字典序词条)→ Posting List(磁盘,文档ID+词频+位置)
★ 为什么比 MySQL LIKE 快:MySQL 全表扫 O(N);倒排查词典 O(1),只读匹配的 Posting List
FST 是有向无环图,同时共享前缀和后缀,比 Trie 压缩率高 10 倍以上
TF(词频)是 BM25 相关性评分核心输入,影响搜索结果排序
Doc Values(正排)= 列式存储,按 doc_id 顺序存字段值,用于排序/聚合(倒排做不了)
写入时 IK 分词器把文档切成词条;查询时把查询词也切成词条,按词条查倒排
⚠️ 易混淆
Term Dictionary 在磁盘,不是内存;只有 Term Index 用 FST 常驻内存
Term Index 存的是词典 Block 偏移,不是 Posting List 地址(Posting List 地址在 Term Dictionary 里)
倒排做全文搜索;Doc Values 做排序聚合,两者并存
🎤 面经
[蚂蚁]R1 #6796 倒排索引了解么
答:关键词 → 文档列表;三层结构 Term Index/Dictionary/Posting List;FST 常驻内存
[字节]R1 #4819 es 倒排索引
答:文档分词后建词条到文档 ID 的映射;查询时词条 → 文档列表合并求交
[美团]R1 #4911 倒排索引、全文索引和聚簇索引有什么区别
答:聚簇索引按主键 B+树存完整行;倒排按词条存文档 ID;全文索引是倒排在 RDBMS 里的实现
[美团]R1 #4924 es 核心原理
答:倒排索引 + 分布式分片 + 近实时(NRT);写入靠 Translog,查询走 Lucene
[B站]R1 #5845 Elasticsearch 倒排索引
答:词条 → 文档 ID + 词频 + 位置;FST 压缩词典,支持快速前缀模糊
[小米]R1 #2072 介绍一下 ES 搜索引擎?ES 的优势是什么?倒排索引?
答:分布式全文搜索;优势是倒排索引 + 近实时 + 聚合强;倒排让模糊匹配 O(1)
[Shopee]R2 #779 ES 中倒排索引原理
答:分词后词条到文档 ID 的映射;查询走 Term Index FST + Term Dictionary Block + Posting List
[字节]R1 #4865 ES 的索引,为什么快
答:倒排把文档关系倒过来;词条字典序排列;FST 内存定位;Posting List 按需读
es-analyzer
🎯 骨架
★ 分词器三件套:Character Filter(字符过滤)→ Tokenizer(分词)→ Token Filter(词条过滤,如小写、停用词、同义词)
★ 内置分词器:standard(默认,按空格+标点)/ keyword(不分词整体)/ english(按英文规则)/ whitespace(仅空格)
★ 中文分词用 IK:ik_max_word(最细粒度,写入用)/ ik_smart(智能切分,搜索用)
写入和搜索分词器分开配置:写入 ik_max_word 切细,搜索 ik_smart 减少召回噪声
自定义词典:IK 支持热更新词典 + 远程词典扩展(电商商品名/人名等业务词)
text vs keyword:text 走分词建倒排(模糊搜索);keyword 不分词整体存(精确匹配/聚合)
fields.keyword 双字段:text + keyword,模糊搜索用 text,精确匹配/聚合用 .keyword
⚠️ 易混淆
standard 分词器对中文按字切,不实用(每个字都成词条)
写入和查询分词器不一致会"明明有这个词搜不到"——必须对齐
keyword 类型不分词,但仍走 doc_values,可排序聚合
🎤 面经
[小米]R1 #2072 介绍一下 ES 搜索引擎?倒排索引?(关联分词器)
答:写入用 ik_max_word 切细建倒排;搜索把查询词分词后查倒排;分词器决定切分粒度
[B站]R1 #5845 Elasticsearch 倒排索引(关联分词)
答:分词后词条到文档 ID 映射;中文用 IK,英文用 standard
[Shopee]R2 #779 ES 中倒排索引原理(关联分词)
答:分词器先把文档切成词条再建倒排;查询时同样分词
[美团]R1 #4924 es 核心原理(关联分词)
答:分词 + 倒排 + 近实时;中文必须用 IK 才能切出有意义的词
[字节]R1 #4865 ES 的索引,为什么快(关联分词)
答:分词后建倒排索引让模糊匹配 O(1);分词粒度决定召回率和精确率
[字节]R1 #4819 es 倒排索引(关联分词)
答:分词后词条字典序存 Term Dictionary;FST 加速词条定位
[蚂蚁]R1 #6796 倒排索引了解么(关联分词)
答:分词 + 倒排;分词器决定词条粒度,IK 是中文标配
es-write-query-flow
🎯 骨架
★ 写入流程:协调节点路由 → 主分片 → 写 Buffer + 写 Translog → 每秒 refresh(生成 Segment 到 Page Cache,可搜索)→ 定期 flush(fsync 到磁盘 + 清 Translog)→ 同步副本
★ NRT(近实时)= 默认 1 秒延迟,refresh 时数据从 Buffer 落到 Segment 才可搜索
★ Translog = WAL 日志,refresh 前宕机靠它恢复,flush 后清空(类似 MySQL redo log)
查询流程(scatter-gather):协调节点广播到所有分片 → 每分片本地查 → 协调节点合并排序 → 返回
Segment 不可变:refresh 生成的 Segment 写到 OS Page Cache,进程崩溃不影响 Page Cache
Buffer 攒批的原因:每写一条就 refresh 会产生大量小 Segment,后台 merge 压力大
主分片数固定不可改:路由公式 hash(doc_id)%分片数;扩容只能 Reindex(scroll+bulk)
⚠️ 易混淆
refresh ≠ flush:refresh 是 Buffer→Segment(可搜索但未持久化);flush 是 Segment→磁盘(持久化)
Segment 在 Page Cache 不在 JVM 堆,进程崩溃 Page Cache 还在,OS 崩才丢
副本同步是同步过程(不是异步),保证 quorum 写入成功才返回
🎤 面经
[B站]R3 #3377 sql 调优 cannal 原理 es 写入原理
答:写 Buffer + Translog → 每秒 refresh 生成 Segment → 定期 flush 持久化 → 同步副本
[美团]R1 #4924 es 核心原理
答:倒排索引 + 分片副本 + NRT;写入靠 Translog WAL,查询广播到所有分片合并
[字节]R1 #4865 ES 的索引,为什么快
答:倒排索引内存定位 + 列式 Doc Values 排序聚合 + Page Cache 缓存 Segment
[字节]R1 #4751 es 的内存占用一直 100% 如何解决
答:JVM 堆 ≤ 32GB(防压缩指针失效)+ 留一半内存给 Page Cache + 关 Fielddata 缓存大字段
[拼多多]R1 #1016 数据库到 ES 的数据同步技术方案,强一致性和最终一致性的取舍
答:Canal 监听 binlog 入 Kafka,消费方写 ES;走最终一致;强一致代价大不实用
[字节]R1 #4750 数据同步怎么做的,如何保证最终一致性
答:binlog → Canal → Kafka → ES;幂等消费 + 重试 + 死信队列保证最终一致
[Shopee]R2 #745 数据到 ES 是怎么流转的?
答:业务库 → Canal → MQ → 消费写 ES;按 doc_id 路由分片 → 主分片写 Translog → refresh
[Shopee]R3 #899 使用 ES 构建索引。基于 binlog 进行构建索引。如何保证数据库和 ES 数据的一致性?
答:binlog 顺序消费 + 幂等写入 + 时间戳兜底重跑;牺牲实时性换最终一致
S2 ES实战与优化
es-vs-mysql
🎯 骨架
★ MySQL 强项:结构化数据 / 事务 / 关联查询 / 强一致性 → 做主存储
★ ES 强项:全文搜索 / 模糊匹配 / 聚合分析(分库分表后 MySQL 聚合慢)→ 做查询引擎
★ 标准架构:MySQL 写入 → Canal 监听 binlog → MQ → 消费方写 ES;MySQL 主存 ES 查询
ES 不能做主存的三大原因:NRT 1 秒延迟 / 不支持事务 / 不支持 JOIN 关联查询
频繁更新场景 ES 性能差:标记删 + 写新 Segment + merge,僵尸文档堆积
ES 聚合是强项不是弱项:分库分表后 MySQL GROUP BY 跨库合并极慢,ES 天然支持
ES 弱项是 JOIN 和事务,不是聚合(常见误区)
⚠️ 易混淆
ES 不是"加强版 MySQL",是不同定位:ES 做搜索/分析,MySQL 做事务存储
ES 没有 JOIN 不是"性能差",是设计上不支持(Nested 和 Parent-Child 也只是模拟)
ES Bool 查询的 must/should/filter 不是 SQL 的关联查询
🎤 面经
[字节]R2 #627 工作中你们的 ES 和 MySQL 之间是怎么用的
答:MySQL 主存事务可靠;ES 查询引擎模糊+聚合;Canal+Kafka 异步同步
[字节]R1 #405 工作中你们的 ES 和 MySQL 之间是怎么用的
答:写 MySQL 是源头,ES 是下游索引;查询时 ES 拿 ID 回 MySQL 取详情
[字节]R1 #4752 es 的作用相较于 mysql 有哪些优点
答:全文搜索 / 模糊匹配 / 聚合 / 分布式扩展;MySQL LIKE 全表扫 ES 倒排 O(1)
[滴滴]R3 #2534 ES 怎么用的?数据量级多少?为什么用 ES 不用 HBase?
答:ES 适合检索 + 排序 + 聚合;HBase 适合 KV 海量存储;选型看是否要全文搜索
[字节]R2 #278 讲了下各个 NOSQL 之间的区别
答:ES 全文搜索;HBase 列存海量;MongoDB 文档;Redis KV 缓存;ClickHouse OLAP
[字节]R1 #659 详细讲讲 ES
答:基于 Lucene 的分布式搜索引擎;倒排索引 + 分片副本 + NRT;适合搜索/聚合/日志
[美团]R1 #4923 什么场景用的 es
答:全文搜索(商品/订单/日志)、聚合分析(订单维度统计)、分库分表后跨库聚合
[美团]R1 #4911 倒排索引、全文索引和聚簇索引有什么区别
答:聚簇索引按主键存完整行(MySQL 主存);倒排按词条存文档 ID(ES 搜索)
es-deep-pagination
🎯 骨架
from+size 的问题:每个分片返回 Top (from+size) 条,协调节点合并后截取,from 越大内存越大,默认限制 from+size ≤ 10000
scroll:创建 doc_id 快照 + pinned IndexReader,游标遍历;不支持实时数据;高并发下堆内存压力大、阻塞 segment merge
search_after:无状态,用上一页最后一条排序值作断点,每次全新查询;只能下一页;排序字段必须唯一且不可变(加 _id 作 tiebreaker)
选型口诀:导出用 scroll,滚动用 search_after,前几页用 from+size;生产推荐 PIT + search_after
search_after ≈ MySQL keyset pagination(WHERE (col, id) > (val, id) LIMIT n),不是游标
PIT = 只 pin 住 segment 视图引用(轻),scroll = 把 TopDocs 存堆内存(重);PIT 代价是磁盘膨胀(旧 segment 不能删),scroll 代价是堆内存压力
⚠️ 易混淆
scroll 快照的是"哪些文档"(doc_id 列表),不是文档内容;内容仍是实时 fetch
search_after 排序字段用 updateTime 有坑:数据更新后排序位置变,可能重复或漏数据
MySQL 游标是服务端有状态的,search_after 是无状态的,两者不等价
🎤 面经
[京东 1面] ES 深分页怎么处理?→ 三种方案:from+size 限前几页,scroll 做导出,search_after 做无限滚动
[字节 1面] ES、MySQL 深度分页如何解决?→ ES 用 search_after,MySQL 用 keyset pagination(WHERE id > last_id),原理相同
[蚂蚁 2面] 深分页问题是什么?有哪些解决方案?→ 深分页 = 跳过大量数据性能差;ES 三方案;MySQL 延迟关联或 keyset
[小红书 1面] ES 深度分页如何优化?→ search_after 替代 from+size,排序字段加 _id 保证唯一
[Shopee 2面] 多表聚合查询深分页如何解决?→ 业务上限制最大页数 + search_after 做无限滚动
es-data-consistency
🎯 骨架
★ ES 不支持 ACID 事务,多文档操作无原子性;只能做最终一致
★ 主从一致性:主分片写完同步到副本(quorum),返回客户端时副本已写入;read 默认从主或副本(默认 round-robin)
★ MySQL→ES 同步链路:业务库 → Canal 监听 binlog → MQ(Kafka)→ 消费方写 ES → 最终一致
同步方案对比:双写(耦合高 + 不一致风险)/ Canal+MQ(解耦 + 最终一致 + 业务无感)
幂等保证:按 doc_id 路由 + 版本号(_version 或 if_seq_no)防覆盖
NRT 延迟:写入后 1 秒(refresh 间隔)才可搜索,业务"刚写完搜不到"是常见坑
强一致代价大:refresh_interval=-1 + force refresh 同步阻塞,吞吐降级 5-10 倍
⚠️ 易混淆
"ES 一致性"分两个层面:① 主从分片副本一致(quorum)② 和外部数据源一致(Canal+MQ)
ES 没有事务回滚,bulk 写一半失败也无法整体撤回
频繁更新性能差(每次 = 标记删 + 写新 Segment + merge),不适合做主存储
🎤 面经
[Shopee]R3 #899 使用 ES 构建索引。基于 binlog 进行构建索引。如何保证数据库和 ES 数据的一致性?
答:binlog 顺序消费 + 幂等写入(_version 或 doc_id 主键)+ 时间戳兜底重跑
[拼多多]R1 #1016 数据库到 ES 的一个数据同步技术方案,强一致性和最终一致性的取舍
答:用 Canal+Kafka 走最终一致;强一致代价大(同步双写或分布式事务),业务可接受秒级延迟
[字节]R1 #4750 数据同步怎么做的,如何保证最终一致性
答:binlog → Canal → Kafka → 消费方幂等写 ES;失败重试 + 死信队列 + 监控对账
[Shopee]R2 #745 数据到 ES 是怎么流转的?
答:业务库 binlog 经 Canal 投递 MQ,下游消费写 ES,幂等保证最终一致
[字节]R2 #627 工作中你们的 ES 和 MySQL 之间是怎么用的
答:MySQL 主存储保事务,ES 查询引擎做模糊+聚合;Canal+Kafka 异步同步
[字节]R1 #405 工作中你们的 ES 和 MySQL 之间是怎么用的
答:同上;MySQL 写入是源头,ES 是下游索引;查 ES 拿 ID 回 MySQL 拿详情
[百度]R2 #7146 es 同步方案里加一个 MQ 中间件的目的是什么?
答:解耦 + 削峰 + 重试容错;Canal 直接写 ES 一旦 ES 抖动 Canal 就堆积,MQ 起缓冲
es-performance-tuning
🎯 骨架
★ 三大方向:Mapping 优化(字段类型 + index/doc_values 配置)/ 查询优化(filter > query)/ 写入优化(Bulk + 关 refresh)
★ filter vs query:都走倒排,区别在是否算 _score;filter 有 bitset 缓存,等值/范围过滤用 filter
★ Bulk 批量导入:refresh_interval=-1 + 副本=0 → bulk(每批 500-1000 条)→ 恢复参数
JVM 堆 ≤ 32GB(防压缩指针失效)+ 留一半内存给 Page Cache(Segment 缓存)
wildcard 前缀通配符 *xxx 退化全量扫描;中文模糊用 ik_max_word + match 替代 wildcard
doc_values 列式存储用于排序聚合;不需要排序聚合的字段关掉省磁盘
单分片 10-50GB 是合理区间,超了考虑按月/按业务拆索引
⚠️ 易混淆
"filter 不算分"不等于"filter 不过倒排"——两者都走倒排,区别只在打分
bitset 缓存绑定 segment,segment 不可变所以 bitset 永远准确
now 每次值不同永远不命中缓存,用 now/m 或 now/h 取整共享缓存
🎤 面经
[字节]R2 #4815 ES 如果存放的数据量过大,可以怎么优化
答:按时间/业务拆索引 + 冷热分离 + force_merge + Reindex 收缩 + ILM 生命周期管理
[字节]R1 #4751 es 的内存占用一直 100% 如何解决
答:JVM 堆 ≤ 32GB + 关 Fielddata + 限制聚合桶数 + 释放 segment merge 后的内存
[字节]R1 #4865 ES 的索引,为什么快
答:倒排索引 + Page Cache + 列式 Doc Values;调优围绕这三个维度
[字节]R1 #4749 es 在你们项目当中有那些业务场景
答:商品搜索 / 订单聚合 / 日志检索 / 用户行为分析;按场景调 Mapping 和分片
[美团]R1 #4923 什么场景用的 es
答:全文搜索 / 模糊匹配 / 聚合 / 跨库统计;调优重点是 Mapping 和查询语句
[字节]R1 #4750 数据同步怎么做的,如何保证最终一致性(关联写入调优)
答:Canal+Kafka+幂等写;批量场景用 Bulk + 关 refresh + 副本=0 加速
[拼多多]R1 #1016 数据库到 ES 的数据同步技术方案(关联写入调优)
答:写入用 Bulk 批量;refresh 拉到 5s+ 减少小 Segment;后台 force_merge 合并
[滴滴]R3 #2534 ES 怎么用的?数据量级多少?为什么用 ES 不用 HBase?
答:选型看是否需要倒排和聚合;调优围绕分片大小、堆内存、Page Cache 三个维度
JAVA-BASIC
S1 对象的基石
equals-hashcode-contract
🎯 骨架
★ 核心契约:equals 相等 → hashCode 必须相等;hashCode 相等 → equals 不一定(哈希冲突)
★ 重写 equals 必须重写 hashCode(否则 HashMap/HashSet 出 bug)
★ equals 五约定:自反、对称、传递、一致、非空
== 比较引用(基本类型比值);equals 默认也是 == (Object.equals),需重写为逻辑相等
HashMap 查找:先算 hashCode 定位桶 → 再用 equals 精确比对
Object 默认 hashCode 由 JVM 实现(与对象内存地址相关,非确定)
⚠️ 易混淆
重写 equals 不重写 hashCode:两个"逻辑相等"对象 hashCode 不同 → 落到不同桶 → get 找不到
IDE 自动生成 equals/hashCode 时要勾选所有业务字段,不要只用 id
String 已经重写 equals(按字符比较)和 hashCode(s[0]*31^(n-1)+...)
equals 不能写成 (this.field == other.field) 比较 String/Integer,应该用 equals
🎤 面经
[B站]R1 #2761 hashcode 与 equal
答:== 比引用;equals 比逻辑相等;重写 equals 必须重写 hashCode 否则 HashMap 失效
[百度]R2 #7149 == 和 equals 之间有什么区别?重写 equals 需要注意什么?同步改什么方法?
答:== 引用 vs equals 逻辑;同时重写 hashCode 满足 equals 相等→hashCode 相等
[蚂蚁]R1 #6867 Hashcode() 和 equals() 和 == 区别?
答:== 比引用/值;equals 默认 == 需重写;hashCode 提供哈希定位用
[快手]R2 #6309 java 为什么重写 equals 和 hashcode,重写要注意什么?
答:HashMap 契约要求;注意五约定+null 检查+用 getClass 判类型
[拼多多]R3 #6922 现场写 equals:cat 和 dog 的 name 一样就 equal
答:先 == 判同对象;再 instanceof 判类型;强转后比 name;同步重写 hashCode
[阿里]R1 #89 hashcode 和 equals 的关系
答:equals 相等 → hashCode 必须相等;HashMap 先查 hashCode 再 equals
[百度]R1 #2459 String 中重写 equals 不重写 hashCode 会出现什么问题
答:放进 HashSet/HashMap 重复元素被认为不同 key,查不到、放不进
string-immutability
🎯 骨架
★ String 是 final 类(不能继承),内部 char[]/byte[] 也是 final 修饰
★ 不可变四大好处:线程安全、可缓存 hashCode、字符串常量池复用、HashMap key 安全
★ 常量池:JDK7 后从永久代移到堆;intern() 把字符串放入常量池
"+" 拼接:编译期常量会优化为一个常量;运行期会转 StringBuilder.append(循环内多次创建)
new String("abc") 创建两个对象(堆中一个 + 常量池可能一个)
拼接选 StringBuilder(单线程)/ StringBuffer(多线程,方法 synchronized)
⚠️ 易混淆
String 不可变不是因为 final 类,是因为内部 value 数组是 final 且不暴露
final char[] 只是引用不可变,内容理论上可改(用反射),但 JDK 不允许外部访问
字符串常量池存的是 String 对象引用(JDK7+),不是 char[]
"abc" + "def" 编译期合并为 "abcdef";str + "x" 运行期转 StringBuilder
🎤 面经
[拼多多]R3 #6924 String 类型是可变吗?为什么不可变?设置为不可变有什么好处?
答:不可变;好处:线程安全、可缓存 hash、常量池复用、当 HashMap key 安全
[拼多多]R1 #1104 String 类型是可变吗?为什么不可变?
答:不可变;final 类+final 数组+不暴露引用;好处线程安全、缓存复用
[京东]R1 #5496 String 为什么是不可变的?StringBuilder 和 StringBuffer 区别?循环内 + 拼接有什么问题?
答:final + 私有 value;循环内 + 每次新建 String 对象,应该用 StringBuilder
[网易]R1 #3267 string 为什么不会变
答:final 类不可继承+private final value 数组+无修改方法对外暴露
[阿里]R1 #255 String hash 底层数据结构 探讨哪个存储更合理
答:String 缓存 hash 字段;不可变保证 hash 一次计算永久有效
[腾讯]R2 #3124 string stringbuffer 的区别
答:String 不可变;StringBuffer 可变+synchronized 线程安全;StringBuilder 可变非同步
[Shopee]R1 #5397 String 能被继承吗?常用的 String 方法?
答:不能(final 类);length/charAt/substring/indexOf/equals/intern 等
string-builder-buffer
🎯 骨架
★ 共同点:都继承 AbstractStringBuilder,可变字符序列,底层 char[]
★ 区别:StringBuilder 非同步(单线程);StringBuffer 方法加 synchronized(线程安全)
★ 性能:StringBuilder > StringBuffer > String(拼接场景)
默认初始容量 16;超出容量按 2 倍 + 2 扩容(Arrays.copyOf)
推荐:单线程拼接用 StringBuilder;多线程基本不用 StringBuffer(用 String 拼接或独立 StringBuilder)
JDK5 引入 StringBuilder,因为大多数场景不需要 StringBuffer 的同步开销
⚠️ 易混淆
StringBuffer 的 synchronized 锁的是 this(当前对象),不是类锁
单线程下 StringBuffer 也安全但有同步开销,不如 StringBuilder
字符串拼接 + 在编译期遇到全是常量会优化为一个 String,但循环内 + 每次创建对象
toString() 不是直接返回 char[],是 new String(value, 0, count) 拷贝一次
🎤 面经
[小米]R1 #3645 String、StringBuilder 和 StringBuff 的区别?
答:String 不可变;StringBuilder 可变非同步快;StringBuffer 可变同步慢
[腾讯]R1 #3862 StringBuilder 和 StringBuffer 的区别?单线程执行有区别吗?
答:是否同步;单线程下都安全但 StringBuffer 有同步开销,应选 StringBuilder
[京东]R1 #5496 String、StringBuffer 和 StringBuilder 的区别?循环内 + 拼接有什么问题?
答:见骨架;循环内 + 每次新建 String 对象,应改 StringBuilder.append
[滴滴]R3 #2573 string、stringbuff、StringBuilder 之间的区别,谁快
答:StringBuilder > StringBuffer > String;前两者可变拼接快
[Shopee]R1 #5397 StringBuilder 和 StringBuffer 的区别?
答:StringBuilder 非同步单线程用;StringBuffer 方法加 synchronized 多线程用
[滴滴]R4 #5462 String 和 StringBuilder
答:String 不可变 + 拼接每次新建对象;StringBuilder 可变 append 高效
[腾讯]R2 #3124 string stringbuffer 的区别
答:String 不可变;StringBuffer 可变+synchronized;性能 SB > Sbf > Str
autoboxing-cache
🎯 骨架
★ 装箱 = int 转 Integer(调 Integer.valueOf(int));拆箱 = Integer 转 int(调 intValue())
★ Integer 缓存范围 -128 ~ 127:valueOf 在范围内返回缓存对象,超出 new
★ 经典坑:Integer.valueOf(127) == Integer.valueOf(127) → true;Integer.valueOf(128) == Integer.valueOf(128) → false
new Integer(127) 绕过缓存,永远是新对象
包装类缓存:Byte/Short/Long 都缓存 -128~127;Character 缓存 0~127;Boolean 缓存 TRUE/FALSE
-XX:AutoBoxCacheMax 可调整 Integer 缓存上限(-128 是固定下限)
⚠️ 易混淆
Integer 比较应用 equals 或 intValue() 比较,不要用 ==
装箱发生时机:赋值、方法参数、返回值、运算(如 Integer + int 时拆箱)
Long 缓存范围 -128~127,不能调;Float/Double 没有缓存
ArrayList 比 int[] 占内存大很多(对象头 + 装箱开销)
🎤 面经
[百度]R2 #7148 Java 的基本类型有哪些?基础类型和引用类型之间的自动装箱机制讲讲
→ 答:8 大基本类型;装箱调 valueOf,拆箱调 xxxValue;Integer 缓存 -128~127
[小米]R1 #5596 自动装箱,自动拆箱
答:装箱 valueOf;拆箱 intValue;Integer 在 -128~127 走缓存返回同对象
[京东]R4 #2141 知道 integer 缓冲机制么?说一下
答:Integer 静态内部类 IntegerCache 数组缓存 -128~127;valueOf 命中范围返回缓存
[腾讯]R2 #3124 int integer;new 两个 integer 相等吗
答:基本类型 vs 包装类;new 出来永远不相等;valueOf 在 -128~127 内会相等
[美团]R1 #4914 Integer a = new Integer(100); a 在哪?Integer b = new Integer(100); a<mark>b?Integer c = 100; a</mark>c?
答:new 在堆;a<mark>b false(两个 new 对象);a</mark>c false(a 堆对象,c 缓存对象)
[网易]R3 #3191 包装类型和基本类型比较问题(Integer == int)
答:== 比较时 Integer 自动拆箱,按 int 值比较,所以 Integer(100) == 100 为 true
[网易]R1 #1446 int 和 Integer 的区别;int 数组和 Integer 数组用 == 比较是否相等?
答:基本类型 vs 包装类;int[] == 比引用,永远 false;Arrays.equals 才能比内容
interface-vs-abstract
🎯 骨架
★ 抽象类用于"是什么"(is-a 继承);接口用于"能干什么"(行为契约)
★ 抽象类可有构造器、成员变量、具体方法、private 方法;接口默认 public
★ 单继承多实现:一个类只能继承一个抽象类,可实现多个接口
JDK8+ 接口可有 default 方法(默认实现)和 static 方法
JDK9+ 接口可有 private 方法(用于 default 方法间复用)
函数式接口:只有一个抽象方法,可用 Lambda(@FunctionalInterface)
⚠️ 易混淆
接口字段默认 public static final(常量);抽象类字段可任意修饰符
接口的 default 方法不能 override Object 的方法(如 toString)
一个类实现多个接口有同名 default 方法 → 编译错误,必须显式重写
abstract 和 final 互斥;抽象类不能实例化但可有构造器(子类调用)
🎤 面经
[网易]R1 #3180 抽象类与接口区别
答:抽象类 is-a 继承+可有状态;接口行为契约+多实现;JDK8 接口可有 default
[Shopee]R1 #5364 接口和抽象类的区别 分别应用场景
答:抽象类共享代码(模板方法);接口定义能力契约(多实现)
[腾讯]R1 #5037 接口和抽象类的区别
答:单继承 vs 多实现;可有状态 vs 默认无状态;构造器 vs 无构造器
[Shopee]R1 #3778 接口和抽象类的区别 分别应用场景
答:抽象类用于复用部分实现;接口用于声明能力(如 Comparable/Runnable)
[小米]R1 #3798 抽象类和接口的区别
答:抽象类是类的扩展(is-a);接口是行为标准(can-do),多实现
[携程]R1 #1793 接口和抽象类的区别
答:抽象类有构造器+成员变量;接口默认 public+JDK8 后可有 default 方法
[字节]R3 #964 Interface 和抽象类的区别?什么场景下用?
答:要共享实现选抽象类;要定义能力契约+多实现选接口;JDK8 后界限模糊
[Shopee]R1 #5392 什么是继承、方法重载与重写、抽象类?
答:继承 is-a;重载同名不同参;重写子类覆盖父类同签名方法;抽象类不能实例化
S2 集合框架的崛起
arraylist-mechanism
🎯 骨架
★ 底层 Object[] 数组,默认初始容量 10(懒加载,第一次 add 才分配)
★ 扩容策略:1.5 倍(oldCap + (oldCap >> 1)),通过 Arrays.copyOf 拷贝
★ 复杂度:随机访问 O(1)、尾部插入均摊 O(1)、中间插入/删除 O(n)
elementData 用 transient + 自定义 writeObject:只序列化 size 个元素,省空间
fail-fast:modCount 字段记录结构修改次数,迭代时变化则抛 CME
线程不安全;并发用 CopyOnWriteArrayList 或 Collections.synchronizedList
⚠️ 易混淆
默认容量 10 是首次 add 才分配,无参构造 new 出来 elementData 是空数组
扩容是 1.5 倍不是 2 倍(HashMap 才是 2 倍)
ArrayList 不是线程安全,多线程 add 会数组越界或丢数据
size() 是 size 字段而非数组长度,elementData.length 可能比 size 大
🎤 面经
[百度]R1 #2840 ArrayList LinkedList 区别
答:ArrayList 数组+1.5 倍扩,O(1) 随机访问;LinkedList 双链表,O(n) 随机
[携程]R1 #2883 数据结构 arraylist 和 linkedlist 的区别
答:底层 vs 链表;随机访问 O(1) vs O(n);缓存友好 vs 节点开销大
[网易]R1 #3270 Arraylist 与 Vector 区别?Arraylist 与 LinkedList 区别?
答:Vector 同步全表锁性能差,已淘汰;ArrayList vs LinkedList 见上
[网易]R3 #3242 ArrayList 和 LinkedList;一亿个 int 要用哪种?
答:用 int[] 直接装,省 1G 装箱开销;非要用 ArrayList,缓存友好胜 LinkedList
[蚂蚁]R1 #4205 Arraylist 和 linkedlist 的区别
答:底层数组 vs 双链表;随机访问 O(1) vs O(n);前者缓存命中率高
[阿里]R4 #6581 ArrayList 和 LinkedList 的区别
答:实现差异+复杂度差异+内存差异;99% 场景 ArrayList 更优
[美团]R1 #4896 二叉树层序遍历用 Arraylist 返回不用 linkedlist
答:返回结果集用 ArrayList 缓存友好;BFS 队列才适合用 ArrayDeque
list-comparison
🎯 骨架
★ 底层:ArrayList = Object[] 数组;LinkedList = 双向链表
★ 随机访问:ArrayList O(1);LinkedList O(n) 必须遍历
★ 中间插入:ArrayList O(n)(数组拷贝);LinkedList O(n) 遍历定位 + O(1) 插入
内存:ArrayList 紧凑、缓存友好;LinkedList 每个节点额外 prev/next 指针 + Node 对象头
实现接口:ArrayList → RandomAccess;LinkedList → Deque(可当队列/栈)
99% 场景选 ArrayList:随机访问 + 缓存命中胜过链表伪 O(1)
⚠️ 易混淆
LinkedList 的"插入 O(1)"是误导:必须先遍历到位置(O(n))才能插
ArrayList 中间删除也要数组拷贝,不是 O(1)
队列/栈场景用 ArrayDeque(循环数组)而非 LinkedList,性能高 ~20%
Vector 已淘汰:synchronized 全表锁性能差
🎤 面经
[百度]R1 #2840 Arraylist linkedlist 区别
答:底层 vs 链表;随机访问 O(1) vs O(n);缓存友好 vs 节点开销
[网易]R3 #3192 ArrayList 和 LinkedList 的比较
答:实现+复杂度+内存+使用场景四方面对比,99% 场景 ArrayList 更优
[网易]R3 #3242 ArrayList 和 LinkedList;一亿个 int 要用哪种?
答:直接 int[] 省装箱;ArrayList 也比 LinkedList 节省内存且更快
[阿里]R4 #6564 linkedlist 和 arraylist 底层的差异
答:数组 vs 双向链表;随机访问 vs 顺序访问;缓存友好 vs 节点散落
[携程]R1 #2883 数据结构 arraylist 和 linkedlist 的区别
答:随机访问 O(1) vs O(n);中间插入两者都 O(n);ArrayList 内存更紧凑
[蚂蚁]R1 #4205 Arraylist 和 linkedlist 的区别
答:底层 + 复杂度 + 缓存命中率三方面,绝大多数场景 ArrayList 更好
[字节]R3 #7112 ArrayList 和 LinkedList 特点及各自应用场景
答:ArrayList 通用首选;LinkedList 仅在需要 Deque 接口时考虑(且 ArrayDeque 更优)
iterator-failfast
🎯 骨架
★ fail-fast:迭代时检测到结构修改(modCount ≠ expectedModCount),立即抛 ConcurrentModificationException
★ 触发条件:迭代过程中通过非迭代器方法修改集合(如 list.remove、list.add)
★ 安全做法:用 Iterator.remove() 或 JDK8 的 removeIf
fail-safe:迭代的是集合副本,不抛异常(CopyOnWriteArrayList、ConcurrentHashMap)
modCount 是 AbstractList 的字段,记录结构修改次数;迭代器初始化时记录快照
fail-fast 只是检测机制,不保证一定抛(多线程下也可能漏检),不是线程安全保证
⚠️ 易混淆
for-each 底层用迭代器,所以也会触发 fail-fast
ConcurrentHashMap 的迭代器是弱一致性,不是 fail-fast 也不是严格 fail-safe
CopyOnWriteArrayList 迭代时看到的是旧数组快照,新写入读不到
modCount 不是同步的,多线程下不可靠
🎤 面经
[蚂蚁]R1 #6886 java 迭代器
答:Iterator 提供 hasNext/next/remove;fail-fast 检测并发修改
[小米]R1 #3629 用 java 实现一个迭代器,并发情况下需要怎么改进
答:基础迭代器维护 cursor;并发改进用 CopyOnWrite 或弱一致性
[滴滴]R3 #2241 List 遍历删除的坑(迭代器失效问题)
答:for-each 中调 list.remove 会抛 CME;用 Iterator.remove() 或 removeIf
[通用]R1 #2241 for-each 中 list.remove 为什么会抛异常?
答:modCount++ 但 expectedModCount 没更新 → next() 检测到不等抛 CME
[通用]R1 #5380 ConcurrentHashMap 的迭代器是 fail-fast 吗?
答:不是,是弱一致性;遍历期间允许其他线程修改,但不保证看到所有更新
[通用]R1 #3192 ArrayList 和 CopyOnWriteArrayList 的迭代器区别
答:前者 fail-fast 检测 modCount;后者 fail-safe 迭代旧数组快照
hashset-impl
🎯 骨架
★ HashSet 底层就是 HashMap,元素作为 key,value 是固定常量 PRESENT
★ 去重依赖 hashCode + equals 双契约(hashCode 不等直接跳过,equals 才精确比对)
★ add 流程:调用 HashMap.put(e, PRESENT),返回 oldValue 是否为 null 决定是否新增
时间复杂度:add/contains/remove 均 O(1)(理想),最坏 O(log n)(红黑树兜底)
不允许重复,允许一个 null(HashMap 允许 null key,0 号桶特殊处理)
线程不安全;并发用 ConcurrentHashMap.newKeySet() 或 Collections.synchronizedSet
⚠️ 易混淆
HashSet 没有自己的存储结构,纯包装 HashMap
去重必须正确实现 equals 和 hashCode(自定义对象常踩坑)
LinkedHashSet 继承 HashSet 但底层是 LinkedHashMap,保留插入顺序
TreeSet 底层是 TreeMap(红黑树),按 key 排序,O(log n)
🎤 面经
[网易]R3 #3188 HashMap 和 HashSet 的实现原理
答:HashSet 内部是 HashMap,元素当 key、value 是 PRESENT 常量
[美团]R4 #3969 hashset 底层实现,hashmap 的 put 操作过程
答:HashSet 直接调用 HashMap.put(e, PRESENT);put 详见 HashMap put 流程
[小米]R1 #3647 Java 中的集合体系,Set 和 List 各有什么特性?
答:Set 不重复(HashSet/TreeSet/LinkedHashSet);List 有序可重复
[拼多多]R3 #6922 重写 equals(联动 HashSet 去重)
答:HashSet 去重靠 equals+hashCode,自定义对象必须同时重写
[百度]R1 #2459 String 中重写 equals 不重写 hashCode 会出现什么问题
答:放进 HashSet 重复元素被误认为不同(落到不同桶),破坏去重语义
[字节]R3 #7112 Java 容器:Set/List/Map 区别(含 HashSet)
答:Set 不重复;HashSet 用 HashMap key 去重;TreeSet 排序;LinkedHashSet 保序
linkedhashmap-lru
🎯 骨架
★ LinkedHashMap = HashMap + 双向链表,保留插入或访问顺序
★ 构造参数 accessOrder=true 时,每次 get 把节点移到链表尾部
★ LRU 实现:继承 LinkedHashMap,重写 removeEldestEntry() 返回 size() > capacity
头部 = 最久未访问,尾部 = 最近访问;超容量自动淘汰头节点
双向链表节点是 Entry 的子类(LinkedHashMap.Entry),多了 before/after 指针
线程不安全;并发场景需 Collections.synchronizedMap 或加锁
⚠️ 易混淆
LinkedHashMap 不是 LinkedList+HashMap 的组合,是 HashMap 的子类(多双向链表)
默认 accessOrder=false(按插入顺序);要 LRU 必须显式传 true
removeEldestEntry 不重写默认返回 false,不会自动淘汰
TreeMap 是按 key 排序(红黑树),LinkedHashMap 是按访问/插入顺序(链表)
🎤 面经
[Shopee]R1 #5380 手撕 LRU:linkedhashmap、hashmap 和双向链表都可以实现
答:继承 LinkedHashMap+accessOrder=true+重写 removeEldestEntry 返回 size>cap
[字节]R1 #378 HashMap 是否无序?LinkedMap 有序的实现?LRU 怎么实现?
答:HashMap 无序;LinkedHashMap 用双向链表保序;LRU 重写淘汰策略
[字节]R1 #6179 手写 LRU
答:HashMap+双向链表;get 移到尾,put 满则淘汰头节点
[蚂蚁]R1 #6856 写题:实现一个 LRU,不让用 IDE,直接手撕
答:双向链表+HashMap,O(1) get/put;尾插头淘汰,访问后移到尾
[美团]R4 #3953 HashMap、ConcurrentHashMap 与 LinkedHashMap 的区别
答:CHM 线程安全;LinkedHashMap 多双向链表保序;HashMap 无序
[滴滴]R3 #2596 treemap 和 linkedhashmap 区别,实现原理
答:TreeMap 红黑树按 key 排序;LinkedHashMap HashMap+双向链表保插入/访问序
[字节]R1 #4803 代码 lru cache 并实现过期失效
答:LinkedHashMap+accessOrder;过期可加 expireTime 字段,get 时判过期
S3 HashMap 深水区
hashmap-principle
🎯 骨架
★ JDK8 数据结构:数组 + 链表 + 红黑树(链表 ≥8 且容量 ≥64 树化,≤6 退化)
★ 默认初始容量 16、负载因子 0.75,容量必为 2 的幂(&(n-1) 替代取模)
★ put 流程:算 hash → 定位桶 → 桶空直接插 / 非空走链表或树 → 判扩容
get 流程:算 hash → 定位桶 → equals 比对 key → 命中返回 value
哈希冲突解决:链地址法(拉链),不是开放寻址
时间复杂度:理想 O(1),最坏链表 O(n)、红黑树 O(log n)
线程不安全:并发 put 可能数据覆盖(JDK8)/ 死循环(JDK7 头插法)
⚠️ 易混淆
HashMap 允许 null key/value(key 落在 0 号桶);Hashtable / ConcurrentHashMap 都不允许
容量是 2 的幂不是为了"美观",是为了 hash & (n-1) 等价于 hash % n 且分布均匀
HashMap 用红黑树(非 AVL):插入/删除旋转更少,统计性能更优
TreeMap 是红黑树整体结构(按 key 排序);HashMap 只在单桶链表过长时才树化
🎤 面经
[小米]R3 #7225 HashMap 的原理讲一下,HashMap 的 get 实现是怎样的过程
答:数组+链表/红黑树;get 算 hash 定位桶,遍历链/树用 equals 比对 key 拿 value
[腾讯]R2 #3118 HashMap 的了解
答:JDK8 是 Node[]+链表+红黑树,put 链表≥8 且容量≥64 转树,并发不安全
[滴滴]R4 #3043 说一下 HashMap 的数据结构、复杂度,从 put 切入
答:数组+链表/红黑树;理想 O(1),链表 O(n),红黑树 O(log n)
[携程]R3 #2917 HashMap 的实现原理,为什么不是线程安全的,并发会有什么问题
答:put 检查桶空非原子,并发会数据覆盖;JDK7 头插还会环形链表死循环
[蚂蚁]R1 #6672 HashMap 底层实现
答:Node[] + 链表 + 红黑树,链地址法解决冲突,2 的幂容量用 &(n-1) 定位
[蚂蚁]R1 #6851 HashMap 怎么防止哈希冲突的,还有什么方法
答:链地址法(拉链);其他方案:开放寻址、再哈希、公共溢出区
[小米]R3 #7236 hashmap 讲一下
答:JDK8 数组+链表/红黑树,2 的幂容量,0.75 负载因子,put 时判扩容判树化
hashmap-redblacktree
🎯 骨架
★ 转树双条件:链表长度 ≥ 8 且 数组容量 ≥ 64(缺一不可,容量小优先扩容)
★ 退化条件:红黑树节点 ≤ 6 时退化为链表(阈值 8/6 留差值,避免临界反复转换)
★ 阈值 8 的依据:泊松分布下链表长度到 8 的概率约 1e-7,正常不会到
用红黑树而非 AVL:插入/删除旋转更少(红黑树最多 3 次,AVL 可能更多)
红黑树插入需要"可比较":默认按 hashCode,相等则按 System.identityHashCode 兜底
单桶最坏:链表 O(n) → 红黑树 O(log n),防止哈希攻击
⚠️ 易混淆
容量 < 64 时即使链表 8 也不树化,先扩容(扩容能分散冲突,比树化更划算)
阈值 8 不是 7 也不是 9:泊松分布计算的概率"突变点"
退化阈值 6 而不是 7:留缓冲,避免在边界来回切换浪费 CPU
TreeNode 占用空间是 Node 的 2 倍(多了 parent/left/right/red 字段)
🎤 面经
[腾讯]R1 #3127 HashMap 的实现讲一下,红黑树的结构、查询性能等
答:链≥8 且容量≥64 转红黑树;查询 O(log n);旋转维持平衡
[百度]R2 #7156 为什么用 B+ 树?和 HashMap 的红黑树的区别?
答:HashMap 红黑树在内存做二叉搜索;B+ 树多路平衡用于磁盘减少 IO
[快手]R2 #6328 说一下 hashmap,红黑树和平衡二叉树有什么区别,为什么不用平衡二叉树?
答:红黑树近似平衡,插入旋转最多 3 次;AVL 严格平衡,旋转开销更大
[京东]R3 #3074 Java 8 中 HashMap 有什么变化?红黑树插入依据什么比较大小?
答:加红黑树+尾插+高位扩容;红黑树按 hashCode 比,相等用 identityHashCode
[网易]R1 #3225 为什么长度达到一定的长度要转化为红黑树
答:链表过长查询退化为 O(n),转红黑树 O(log n),防止哈希攻击
[字节]R3 #333 为什么 HashMap 要用红黑树,为什么不用二叉平衡树
答:红黑树插入/删除旋转更少;二叉平衡(AVL)严格平衡维护代价高
[携程]R1 #5273 链表什么时候转化为红黑树,红黑树的好处,红黑树如何搜索
答:链≥8 且容量≥64 转树;最坏 O(log n);按 key 比较走左右子树
hashmap-hash-design
🎯 骨架
★ JDK8 hash 函数:(h = key.hashCode()) ^ (h >>> 16),一行扰动
★ 目的:高 16 位异或低 16 位,让 hash 的高位也参与索引计算(&(n-1) 只用低位)
★ 容量必为 2 的幂:(n-1) & hash 等价于 hash % n,但位运算更快
容量是 2 的幂的另一好处:n-1 是低位全 1 的掩码,hash 分布更均匀
扰动一次的代价权衡:JDK7 扰动 4 次太重;JDK8 简化成 1 次,配合红黑树兜底
⚠️ 易混淆
索引公式 (n-1) & hash 只取低位 → 如果不扰动,hash 高位永远不参与,容易冲突
key.hashCode() 由 key 类型决定(String、Integer 各自重写),不是 HashMap 的事
null key 走特殊路径,hash 强制为 0,固定到 0 号桶
不要把扰动函数和定位函数混淆:扰动是 hash() 方法,定位是 putVal 里的 &
🎤 面经
[小米]R3 #3154 HashMap 为什么长度是 2 的 n 次幂,数据结构,扩容
答:2 的幂下 (n-1)&hash 等价于 % n 且位运算更快、分布更均匀
[小米]R1 #3652 HashMap 为什么长度是 2 的 n 次幂
答:n-1 低位全 1,&hash 等价取模且只动低位,扩容时高位判定 0/1 也方便
[美团]R4 #1336 hashmap 为什么是 2 的 n 次方
答:&(n-1) 替代 mod 提速;扩容时 hash&oldCap 判断元素新位置
[Shopee]R1 #5420 为什么 hashmap 计算 hashcode 要计算 hashcode 后高位运算再 &(old capacity)
答:&(n-1) 只用低位,高位异或让高位也参与,减少冲突
[京东]R3 #3074 hashmap 处理哈希冲突用哪种方法?时间复杂度?
答:链地址法(拉链);理想 O(1),链表 O(n),红黑树 O(log n)
[蚂蚁]R1 #6851 HashMap 怎么防止哈希冲突的,还有什么方法
答:链地址法+扰动函数+红黑树;其他方案有开放寻址、再哈希、公共溢出区
hashmap-resize
🎯 骨架
★ 触发条件:size > threshold(容量 × 0.75)
★ 扩容策略:容量翻倍(2 倍),threshold 也翻倍
★ JDK8 迁移优化:用 hash & oldCap 判位置,结果 0 索引不变,结果非 0 索引 += oldCap
JDK7 迁移:每个元素重新 hash 算位置 + 头插法(多线程会形成环)
扩容代价:O(n) 全部元素迁移;预估容量可避免多次扩容
推荐写法:new HashMap<>((int)(expected/0.75)+1) 或 Maps.newHashMapWithExpectedSize(n)
⚠️ 易混淆
翻倍后元素只有两种去向:"原位"或"原位 + oldCap",不会去其他位置
扩容会重新分配数组并迁移,不是原地扩,所以代价是 O(n)
5 万条数据指定容量应是 ceil(50000/0.75)+1=66668,HashMap 会向上取到 131072
负载因子 0.75 是时间和空间的折中:太小空间浪费,太大冲突多
🎤 面经
[阿里]R2 #27 HashMap 扩容机制;为什么扩容是 2 倍;多线程 put 会引发什么问题
答:2 倍方便用 hash&oldCap 判迁移位置;JDK7 多线程扩容会形成环
[快手]R2 #6343 默认长度多少?长度不够怎么办?怎么判断长度不够?扩容的具体过程
答:默认 16;size > 12 触发;2 倍扩+迁移用 hash&oldCap 判 0 不变 1 加 oldCap
[小米]R3 #3154 HashMap 扩容包括元素移动的细节
答:2 倍扩;JDK8 用 hash&oldCap:0 → 索引不变;非 0 → 索引+oldCap
[Shopee]R1 #5420 Java 1.7 和 1.8 的 hashmap resize 区别
答:1.7 重算 hash+头插;1.8 用 hash&oldCap 判位置+尾插,避免环
[小米]R1 #3652 HashMap 扩容(包括元素移动的细节)
答:2 倍扩;hash&oldCap=0 留原位,=非 0 迁到 i+oldCap,无需重算 hash
[快手]R2 #2750 HashMap 在 1.7 和 1.8 中的区别?链表环是怎么形成的?
答:1.7 头插+并发扩容→A.next=B、B.next=A 倒置成环;1.8 尾插无此问题
[other]R1 #4356 hashmap 扩容是怎么实现的
答:新数组容量 2 倍 → 遍历旧表 → hash&oldCap 判位置 → 尾插到新桶
hashmap-put-get-flow
🎯 骨架
★ 算 hash:(h = key.hashCode()) ^ (h >>> 16)(高 16 位异或低 16 位扰动)
★ 定位桶:(n - 1) & hash,n 必须是 2 的幂
★ 桶空 → 直接 newNode 放入;桶非空 → 链表尾插或红黑树插入
判树化:链表长度 ≥ 8 且容量 ≥ 64 才转红黑树(容量 < 64 优先扩容)
★ 判扩容:size > threshold(容量 × 0.75)触发 resize(2 倍扩容)
算 hash → 定位桶
比对头节点 key(==或 equals)
命中返回;未命中沿链表/红黑树继续找,找不到返 null
⚠️ 易混淆
算 hash 用的是 (h = key.hashCode()) ^ (h >>> 16),不是直接用 hashCode
key 是 null 时 hash = 0,固定落到 0 号桶(HashMap 允许 null key)
equals 之前先比 hash 是否相等(hash 不等直接跳过)
树化前置条件:必须容量 ≥ 64,否则只扩容不树化
🎤 面经
[百度]R2 #7181 Map put 的流程讲下
答:算 hash → &(n-1) 定位桶 → 桶空插入 / 非空尾插 → 判树化 → 判扩容
[携程]R1 #5273 HashMap 里 put 一个元素的整个流程,链表什么时候转化为红黑树
答:算 hash 定位桶,桶空直插,非空走链/树;链≥8 且容量≥64 转红黑树
[京东]R1 #1970 HashMap 中 put 的原理;HashMap 源码
答:putVal 五步:算 hash → 定位桶 → 插入 → 判树化 → 判扩容
[快手]R2 #6343 put 实现原理;默认长度多少?长度不够怎么办?
答:默认 16;size > 16×0.75=12 触发 resize 扩容到 2 倍
[小红书]R1 #5335 HashMap 的底层数据结构,put 的过程
答:Node[]+链表/红黑树;put 算 hash → &(n-1) 定位 → 插入 → 判扩容
[字节]R3 #333 HashMap put get 过程;为什么用红黑树不用平衡二叉树
答:put 见骨架;红黑树插入旋转更少,比 AVL 树插入维护开销小
[小米]R3 #7225 HashMap 的 get 实现
答:算 hash → 定位桶 → 比对头节点 → 沿链表/红黑树查找 → 命中返回 value
hashmap-jdk7vs8
🎯 骨架
★ 数据结构:JDK7 数组+链表;JDK8 数组+链表+红黑树(链≥8 且容量≥64 转树)
★ 插入方式:JDK7 头插法(多线程下会形成环);JDK8 尾插法(避免环形链表)
★ 扩容机制:JDK7 重算 hash;JDK8 用 hash & oldCap,结果 0 索引不变、1 索引+oldCap
hash 函数:JDK7 多次扰动;JDK8 高 16 位异或低 16 位(一次扰动)
并发问题:JDK7 死循环 + 数据丢失;JDK8 仅数据丢失(消除死循环)
链表查询长度阈值:JDK8 引入红黑树后,单桶最坏 O(log n);JDK7 一直 O(n)
⚠️ 易混淆
JDK8 改尾插不是为了性能,是为了消除并发扩容时的环形链表
JDK8 仍然不是线程安全的,只是去掉了"死循环"这一个问题
JDK8 扩容利用率高位是因为容量翻倍后 hash 的"新增一位"决定去留
🎤 面经
[快手]R2 #2750 HashMap 在 1.7 和 1.8 中的区别是啥?链表环是怎么形成的?
答:1.7 头插+rehash 多线程会形成环导致 get 死循环;1.8 改尾插+高位判断避免
[快手]R2 #2751 HashMap 怎么实现线程安全的?1.7 和 1.8 的区别?
答:HashMap 本身都不安全;1.8 引入红黑树+尾插+高位判断扩容
[快手]R1 #2745 1.7 里 HashMap 链表有环的问题展开说说
答:扩容并发,A 线程 next=B、B 线程 rehash 后 B.next=A,头插倒置形成环
[蚂蚁]R1 #7099 HashMap 1.7 和 1.8 区别
答:1.8 加红黑树、改尾插、用高位判断扩容、消除死循环、hash 算法简化
[蚂蚁]R1 #6860 jdk1.7 到 jdk1.8 Map 发生了什么变化(底层)
答:数据结构加红黑树、头插改尾插、扩容用 hash&oldCap、hash 函数简化
[快手]R2 #6329 HashMap 链表插入方式为什么是尾插?
答:尾插保留原顺序,扩容迁移不会颠倒链表,避免多线程下形成环
[other]R1 #4356 Hashmap 在 1.8 中除了引入红黑树、还有其他改动吗?为什么头插要改尾插?
答:改动还有:高位判断扩容、hash 函数简化、扩容时序优化;尾插避免链表环
hashmap-thread-unsafe
🎯 骨架
★ JDK8 并发 put 数据丢失:检查桶空 + 写入两步非原子,后写覆盖先写
★ JDK7 并发扩容死循环:头插法+rehash 颠倒链表,A.next=B、B.next=A 形成环
★ size 不准:多线程同时 put,size++ 不是原子操作
线程安全替代:Hashtable(全表锁,废)/ Collections.synchronizedMap(全表锁)/ ConcurrentHashMap(桶级锁,首选)
fail-fast:迭代时其他线程修改触发 modCount 变化 → ConcurrentModificationException
解决思路:业务层用 ConcurrentHashMap;不可变场景可用 ImmutableMap
⚠️ 易混淆
JDK8 已经修复死循环,但仍然不是线程安全(数据覆盖问题还在)
HashMap 的复合操作(先 get 再 put)即使换 ConcurrentHashMap 仍非原子,要用 computeIfAbsent
fail-fast 只是检测机制,不保证一定抛异常(也不保证线程安全)
🎤 面经
[百度]R1 #2842 HashMap 线程安全吗?什么是线程安全和不安全?什么结构导致线程不安全?
答:不安全;put 检查空+写入非原子并发会丢数据;用 ConcurrentHashMap
[蚂蚁]R1 #6741 HashMap 为什么不是线程安全的?怎么让 HashMap 变得线程安全?
答:put 非原子;可用 Hashtable / synchronizedMap / ConcurrentHashMap
[携程]R3 #2917 HashMap 实现原理,为什么不是线程安全的,并发情况下会有什么问题
答:put 非原子并发会数据覆盖;JDK7 头插还会形成环导致 get 死循环
[快手]R1 #2745 1.7 里 HashMap 链表有环的问题展开说说
答:扩容并发→A 看到 B 是 next、B 已 rehash 后 next 指 A→头插倒置成环
[拼多多]R3 #7024 HashMap 多线程有什么问题?怎么解决?
答:JDK7 死循环+数据丢失,JDK8 仍数据丢失;用 ConcurrentHashMap
[小米]R3 #3154 HashMap 线程不安全的问题
答:JDK7 并发扩容形成环;JDK8 并发 put 数据覆盖;要用 CHM
[快手]R2 #2751 HashMap 怎么实现线程安全的?1.7 和 1.8 的区别?
答:本身都不安全,需用 CHM;1.8 改用 CAS+synchronized 桶级锁
S4 并发集合与线程安全
concurrent-hashmap-jdk7vs8
🎯 骨架
★ JDK7:Segment 分段锁(ReentrantLock),默认 16 段,并发度 16,段内扩容
★ JDK8:Node[] + CAS + synchronized,锁粒度降到桶级,并发度 = 桶数量
★ JDK8 put 流程:桶为空 → CAS 写入(无锁);桶非空 → synchronized(桶头节点) 操作链表/红黑树
get 不加锁:Node.val 和 Node.next 是 volatile,保证可见性
size():JDK8 用 baseCount + CounterCell[](类 LongAdder),分散计数减少竞争
为什么 JDK8 改:Segment 数量固定并发度有上限 + 内存开销大 + JDK8 synchronized 性能不差
⚠️ 易混淆
ConcurrentHashMap 的单个操作线程安全,但复合操作不原子(先 get 再 put 要用 computeIfAbsent)
JDK8 已经用了 synchronized 为什么还要 CAS?→ 桶为空时 CAS 更快(无竞争),桶非空才用 synchronized
不允许 null key/value(Hashtable 也不允许,HashMap 允许)
🎤 面经
[美团][R2] JDK7 分段锁与 JDK8 CAS+synchronized 的实现差异?→ 段级锁 vs 桶级锁,ReentrantLock vs CAS+synchronized
[字节][R2] 已经用了 synchronized 为什么还要用 CAS?→ 桶为空时 CAS 无竞争更快,桶非空才 synchronized 保证原子
[网易][R1] ConcurrentHashMap 的 get 为什么不加锁仍安全?→ Node.val 和 next 是 volatile,保证可见性
[腾讯][R1] ConcurrentHashMap 的实现原理?→ JDK7 分段锁,JDK8 CAS+synchronized 桶级锁
[通用][R1] ConcurrentHashMap 复合操作线程安全吗?→ 不安全,先 get 再 put 两步非原子,要用 computeIfAbsent
concurrent-hashmap-source
🎯 骨架
★ JDK8 数据结构:Node[] + 链表 + 红黑树(同 HashMap),废弃 Segment
★ put 锁策略:桶为空 → CAS 插入(无锁);桶非空 → synchronized 锁桶头节点
★ get 不加锁:Node.val 和 Node.next 都是 volatile,写对读立即可见
size() 实现:baseCount + CounterCell[](类 LongAdder),分散计数避免热点
协助扩容:put 时若发现正在扩容(sizeCtl < 0),帮助迁移其他桶(多线程并行)
不允许 null key/value:避免 get 返回 null 的二义性(key 不存在 vs value 为 null)
⚠️ 易混淆
已经用 synchronized 为什么还要 CAS:桶为空时 CAS 无竞争更快
用 synchronized 不是 ReentrantLock:JDK6 后 synchronized 经过锁升级优化,性能不差
size() 不是直接遍历计数,而是累加 baseCount + 所有 CounterCell.value
复合操作(先 get 再 put)仍非原子,要用 putIfAbsent / computeIfAbsent
🎤 面经
[腾讯]R1 #3127 hashmap 的实现讲一下;红黑树的结构、查询性能等(包含 ConcurrentHashMap)
答:CHM JDK8 用 Node[]+CAS+synchronized 桶级锁,get 靠 volatile 读
[百度]R2 #7200 Java8 之后的 ConcurrentHashMap 舍弃分段锁的原因
答:Segment 数量固定限制并发度+内存大;synchronized 升级后性能不差
[小红书]R1 #4078 1.8 之前是分段锁、1.8 之后是局部锁,锁的是什么对象?
答:1.7 锁 Segment 段;1.8 锁桶头 Node(synchronized 锁链表/红黑树头)
[蚂蚁]R1 #6663 concurrenthashmap 1.8 的改动
答:废 Segment;改 Node[]+CAS+synchronized;锁粒度到桶;引入红黑树
[拼多多]R3 #7024 ConcurrentHashMap 怎么实现线程安全的
答:桶空 CAS 插入;桶非空 synchronized 锁桶头;get 不锁靠 volatile
[快手]R2 #6329 Concurrenthashmap 是如何加锁,put 和 get 有什么区别?
答:put 锁桶头(CAS+sync);get 不锁,volatile 读保证可见性
[蚂蚁]R1 #6741 jdk1.8 对 ConcurrentHashMap 做了哪些优化
答:废分段锁→桶级锁;CAS+synchronized;红黑树;CounterCell 分散 size
copyonwrite-arraylist
🎯 骨架
写时复制:写操作(add/set/remove)加 ReentrantLock,复制一份新数组修改后替换引用
读不加锁:读操作直接读旧数组,读写不互斥,适合读多写少场景
代价:写操作内存翻倍 + 数组复制开销;读到的可能是旧数据(弱一致性)
⚠️ 易混淆
不保证实时一致性,写完后其他线程不一定立刻读到新值(读的是旧快照)
扩容:没有扩容概念,每次写都是 Arrays.copyOf(old, old.length + 1)
🎤 面经
[小红书 1面] CopyOnWriteArrayList 的适用性和缺陷?→ 适合读多写少;缺陷:写时内存翻倍、弱一致性(读旧快照)
[美团 2面] Copy-On-Write 机制的原理、应用场景以及涉及的数据结构设计?→ 写时复制新数组,读旧数组不加锁;适合配置/白名单等读多写少场景
[小米 1面] CopyOnWriteList 怎么保证线程安全?为什么这么做?→ 写加 ReentrantLock + 复制新数组;读不加锁,用空间换并发读性能
[京东 1面] CopyOnWriteArrayList 怎么做扩容的?→ 没有扩容,每次 add 都 copyOf(old, old.length+1),新建一个长度+1 的数组
[字节 1面] CopyOnWriteArrayList 和 Collections.synchronizedList 的区别?→ COW 读不加锁性能好但弱一致;synchronizedList 读写都加锁强一致但并发差
hashtable-vs-chm
🎯 骨架
Hashtable:所有方法加 synchronized(全表锁),并发性能差,已淘汰
ConcurrentHashMap JDK7:分段锁(Segment),16个段,段级别加锁,并发度=16
ConcurrentHashMap JDK8:CAS + synchronized,锁粒度到桶(Node),并发度=桶数量
桶为空时 CAS 插入(无锁)
桶非空时 synchronized 锁链表/红黑树头节点
get 不加锁:Node.val 和 Node.next 用 volatile 修饰,保证可见性
不允许 null key/value:防止二义性(get 返回 null 无法区分 key 不存在还是 value 为 null)
⚠️ 易混淆
JDK8 CHM 用 synchronized 不是 ReentrantLock(JDK7 Segment 继承 ReentrantLock)
size() 用 CounterCell 分段计数,不是直接加锁统计
🎤 面经
[字节 2面] ConcurrentHashMap 怎么保证线程安全?已用 synchronized 为什么还用 CAS?→ 桶为空时 CAS 无竞争更快,桶非空才 synchronized;两者配合减少锁竞争
[网易 1面] ConcurrentHashMap 的 get 为什么不加锁仍安全?→ Node.val 和 next 是 volatile,写操作对读线程立即可见
[美团 2面] JDK7 分段锁与 JDK8 CAS+synchronized 的差异?→ JDK7 段级锁并发度=16;JDK8 桶级锁并发度=桶数,更细粒度
[腾讯 2面] HashMap/Hashtable/ConcurrentHashMap 三者差异?→ HashMap 非线程安全;Hashtable 全表锁性能差;CHM 细粒度锁高并发
[腾讯 1面] ConcurrentHashMap 实现原理?→ JDK8:数组+链表/红黑树,CAS插入+synchronized锁桶头,volatile保证可见性
[拼多多 1面] CHM 不允许 null key/value 的原因?→ 防二义性:get(key)=null 无法区分 key 不存在还是 value 为 null,多线程下更危险
blocking-queue
🎯 骨架
★ 阻塞语义:队列空时 take 阻塞、队列满时 put 阻塞,用 Condition 实现
★ 常用实现:ArrayBlockingQueue(数组有界)/ LinkedBlockingQueue(链表可选有界)/ SynchronousQueue(无缓冲,直接交付)/ PriorityBlockingQueue(堆)
★ 线程池工作队列:FixedThreadPool→LinkedBlockingQueue(无界);CachedThreadPool→SynchronousQueue(无缓冲);ScheduledThreadPool→DelayedWorkQueue
实现核心:ReentrantLock + 两个 Condition(notEmpty、notFull);put 唤醒 notEmpty,take 唤醒 notFull
核心方法四组:抛异常(add/remove)、返回特殊值(offer/poll)、阻塞(put/take)、超时(offer/poll with timeout)
ArrayBlockingQueue 单锁(put/take 共用);LinkedBlockingQueue 双锁分离(吞吐更高)
⚠️ 易混淆
LinkedBlockingQueue 默认容量是 Integer.MAX_VALUE,等同无界,会导致 OOM
SynchronousQueue 容量为 0,put 必须等到一个 take 配对才返回
ArrayBlockingQueue 是循环数组,head/tail 指针配合 % capacity
BlockingQueue 不允许 null,put null 会抛 NPE
🎤 面经
[蚂蚁]R1 #6791 项目里多线程涉及 BlockingQueue,它的特点是什么?
答:满则 put 阻塞、空则 take 阻塞;ReentrantLock+Condition 实现
[快手]R2 #6376 单线程池和固定线程池用 LinkedBlockingQueue,缓存线程池用 SynchronousQueue 为什么?
答:Fixed 线程数固定要缓冲任务;Cached 直接交付,无任务回收线程
[阿里]R5 #222 BlockingQueue 相关知识
答:满阻塞空阻塞,常用 ArrayBQ/LinkedBQ/SyncQ;线程池工作队列
[蚂蚁]R1 #7100 BlockingQueue 相关知识
答:核心 4 组方法(抛异常/特殊值/阻塞/超时);底层 ReentrantLock+Condition
[Shopee]R1 #792 ArrayBlockingQueue vs LinkedBlockingQueue,电商秒杀场景如何选择?
答:ArrayBQ 单锁内存固定不会 OOM;LinkedBQ 双锁吞吐高但默认无界要显式限容
[百度]R2 #7159 阻塞队列了解吗?常见的阻塞队列算法有哪些?
答:ArrayBQ/LinkedBQ/SyncQ/PriorityBQ/DelayQueue;底层 Lock+Condition
[蚂蚁]R1 #6792 手撸阻塞队列,实现 put 和 take 方法
答:用 ReentrantLock+notEmpty/notFull 两个 Condition;满 await,操作后 signal
[蚂蚁]R1 #6730 阻塞队列不用 java 提供的自己怎么实现?condition 和 wait 不能用
答:synchronized + wait/notify;或 LockSupport park/unpark + 状态机
S5 语言特性与设计模式
generics-type-erasure
🎯 骨架
★ 类型擦除:泛型是编译期特性,运行时类型信息被擦除
★ List 和 List 运行时都是 List,无法通过 instanceof 区分
★ 擦除原因:JDK5 引入泛型时为向后兼容 1.4 之前的代码(伪泛型)
擦除后类型替换为上界(无界擦为 Object,`` 擦为 Number)
字段/方法签名的泛型保留在 class 字节码 Signature 属性中,反射可读
通配符 PECS:<? extends T> 生产者只读;<? super T> 消费者只写
⚠️ 易混淆
泛型不能用基本类型(int 不行,要用 Integer)
不能 new T()(运行时不知道 T 是什么),要传 Class 才能反射创建
不能 T[] 直接 new(数组是协变的会破坏类型安全)
桥方法(Bridge Method):编译器为保证多态自动生成
🎤 面经
[携程]R1 #2916 泛型与 PECS
答:Producer Extends Consumer Super;只读用 extends,只写用 super
[携程]R1 #5270 java 泛型原理,什么时候不会进行类型擦除
答:编译期检查+运行期擦除;字段/方法签名保留 Signature,反射 Type 可拿到
[拼多多]R3 #1240 为什么称 java 泛型为伪泛型?泛型的好处?int 可以作为泛型类型吗?
答:运行时擦除→伪泛型;好处编译期类型安全;int 不行需 Integer(基本类型不能泛型)
[通用]R1 #5270 TypeReference 泛型解析过程
答:用匿名内部类继承泛型类,子类 Signature 保留泛型,反射 getGenericSuperclass 获取
[通用]R1 #5270 泛型 extends 和 super 用来干什么
答:extends 上界保证类型在 T 之下(生产者只读);super 下界(消费者只写)
[通用]R1 #1240 泛型 List 和 List 运行时一样吗
答:一样,都擦除为 List;不能用 instanceof List 判断
[通用]R1 #1240 泛型为什么不支持基本类型
答:擦除替换为 Object,基本类型不是 Object 子类;只能用包装类
reflection-mechanism
🎯 骨架
★ 反射 = 运行时拿到类的字段/方法/构造器并动态调用
★ 三种获取 Class:Xxx.class / obj.getClass() / Class.forName("全限定名")
★ 性能开销:Method.invoke() 比直接调用慢 5-10 倍,要缓存 Method 对象
setAccessible(true) 绕过 private 访问控制
主要用途:Spring IoC(反射创建 Bean+注入)、ORM(字段映射)、JSON 序列化
JDK9 模块化后,跨模块反射受 module-info 限制(--add-opens 解锁)
⚠️ 易混淆
Class.forName 默认会触发类初始化(执行 static 块);ClassLoader.loadClass 不会
Method/Field 不缓存的话每次 getMethod 都要遍历方法表,性能差
反射调用 private 字段成功,但 final 字段在 JIT 优化后改也无效(要禁用 inline)
newInstance() 已废弃,推荐 getDeclaredConstructor().newInstance()
🎤 面经
[小米]R3 #7218 反射定义,spring 中哪些地方用到了反射,自己在哪里用到反射
答:运行时操作类的能力;Spring IoC 创建 Bean+注入靠反射;自己写 BaseDao 通用 CRUD
[快手]R1 #2736 反射的优缺点和应用场景;jdk 动态代理和 cglib 的区别
答:优点:灵活动态;缺点:性能差+破坏封装;用于框架;JDK 代理基于接口、CGLIB 继承
[蚂蚁]R1 #6883 java 反射,深拷贝浅拷贝
答:反射运行时操作类;深拷贝靠反射递归克隆字段;浅拷贝直接字段值复制
[网易]R1 #3216 JVM 内存结构和 Java 反射机制体现在 Spring 中哪些方面?
答:Spring 用反射扫描注解+创建 Bean+注入字段;BeanDefinition 反射读字段
[百度]R1 #2845 Java 反射机制如何获取到类
答:三种方式:Xxx.class / obj.getClass() / Class.forName(全限定名)
[滴滴]R3 #2595 反射了解么,原理是什么
答:JVM 类加载后保留 Class 元数据,反射 API 访问字段/方法/构造器并动态调用
[得物]R1 #5826 反射的概念和实现
答:运行时获取类信息;通过 Class 对象拿 Field/Method/Constructor,调 invoke
[网易]R1 #1464 Java 反射原理
答:JVM 加载 .class 后存 Class 元数据;反射 API 暴露元数据并支持动态调用
dynamic-proxy
🎯 骨架
★ JDK 动态代理:基于接口,运行时生成代理类(实现目标接口),调用 InvocationHandler.invoke
★ CGLIB:基于继承,运行时字节码生成目标类的子类,重写方法走 MethodInterceptor
★ Spring AOP:有接口默认 JDK 代理;无接口(或 proxy-target-class=true)用 CGLIB
JDK 代理三要素:ClassLoader、Interfaces、InvocationHandler;通过 Proxy.newProxyInstance 创建
CGLIB 限制:目标类不能 final(无法继承);private/static 方法不能代理
性能:JDK8+ JDK 代理性能不差于 CGLIB;CGLIB 首次生成字节码慢
⚠️ 易混淆
静态代理在编译期写好;动态代理运行时生成
JDK 代理一定要有接口;类没接口只能 CGLIB
AOP 切到自己类内部方法(this.method)不会走代理(要 AopContext.currentProxy() 拿代理对象)
@Transactional 自调用失效就是因为没走代理
🎤 面经
[快手]R1 #2736 JDK 动态代理和 cglib 的区别
答:JDK 基于接口生成实现类,CGLIB 基于继承生成子类;CGLIB 不能代理 final
[蚂蚁]R1 #6644 AOP 的底层实现机制;动态代理和静态代理的区别;动态代理的实现方式
答:静态代理编译期写死;动态代理运行时生成;JDK Proxy + CGLIB 两种
[网易]R3 #3189 动态代理的原理
答:JDK Proxy.newProxyInstance 生成实现接口的代理类,调用走 InvocationHandler
[百度]R1 #2856 动态代理的几种方式
答:JDK Proxy(接口)+ CGLIB(继承)+ Javassist + ByteBuddy(字节码工具)
[百度]R1 #2846 Java 动态代理了解吗
答:运行时生成代理类增强目标方法;JDK Proxy 基于接口;CGLIB 基于继承
[京东]R1 #4005 spring 的 aop 原理?动态代理都有什么?区别是什么?
答:AOP 靠动态代理;JDK Proxy 接口+反射调用;CGLIB 继承+字节码生成子类
[携程]R1 #7264 AOP 动态代理的实现
答:Spring 启动时为切点匹配的 Bean 生成代理;运行时拦截方法调用前后织入
[蚂蚁]R2 #4118 java 动态代理
答:Proxy.newProxyInstance 接受 ClassLoader/Interfaces/InvocationHandler 三参生成代理
exception-hierarchy
🎯 骨架
★ Throwable 根,下分 Error(JVM 错误,OOM/StackOverflow,不该 catch)和 Exception
★ Exception 分两类:Checked(受检,编译强制处理,如 IOException)和 Unchecked(运行时 RuntimeException,可不处理)
★ try-catch-finally:finally 必执行(除非 System.exit 或 JVM 崩溃);finally 中 return 会覆盖 try 的 return
throw 抛异常对象;throws 方法签名声明(受检异常)
自定义异常:业务异常继承 RuntimeException(不强制处理,简洁)
JDK7 try-with-resources:自动关闭实现 AutoCloseable 的资源
⚠️ 易混淆
Error 不是 Exception 的子类,二者都继承 Throwable
NullPointerException、ClassCastException、ArrayIndexOutOfBoundsException 都是 RuntimeException
finally 中改变基本类型 return 不影响 try 已 return 的值;改对象引用会影响(不是值)
catch (Exception e) 不能捕获 Error;要捕获 OOM 用 catch (Throwable)
🎤 面经
[小米]R1 #3646 Java 中的异常有哪些分类?常见的运行时异常有哪些?
答:Error vs Exception;Exception 分受检和非受检;NPE/CCE/AIOOBE/CME/IAE
[阿里]R4 #2957 关于 Java 异常的继承层次结构
答:Throwable → Error/Exception → RuntimeException → 各种具体异常
[网易]R1 #3250 异常和 error 的区别,oom 是 error 还是异常?
答:Error 是 JVM 层不可恢复;OOM 是 Error;Exception 是程序可处理的
[网易]R1 #3185 exception 的根类是哪个类?1/0 是哪类异常?
答:Exception 父类是 Throwable;1/0 抛 ArithmeticException(运行时异常)
[快手]R2 #6353 Java 异常类分为哪两种?有什么区别?
答:Checked 编译强制处理;Unchecked(RuntimeException)可不处理
[滴滴]R1 #3017 synchronized 里发生异常会有问题吗?会释放锁吗?
答:会释放锁(JVM 在异常退出时自动释放 monitor);ReentrantLock 不会,必须 finally unlock
[滴滴]R1 #2980 Java 异常
答:Throwable 体系;Checked 编译强制 + Unchecked 运行时;try-catch-finally
io-nio-aio
🎯 骨架
★ BIO:同步阻塞,一连接一线程,简单但连接数多时线程爆炸
★ NIO:同步非阻塞,Selector 多路复用,一线程管多个 Channel(Netty 基础)
★ AIO:异步非阻塞,回调通知;Linux 实现仍底层用 epoll,业界用得少
NIO 三大组件:Channel(双向通道)、Buffer(缓冲区)、Selector(多路复用器)
NIO 底层 epoll(Linux)/ kqueue(Mac),事件驱动
选型:连接数多、长连接 → NIO;连接少、短连接 → BIO 也行(简单)
⚠️ 易混淆
"同步/异步"是关注消息通信机制;"阻塞/非阻塞"是关注线程状态
NIO 的 N 是 New 不是 NonBlocking(虽然实际就是 NonBlocking IO)
ByteBuffer 用 flip() 切换读写模式(position=0、limit=之前的 position)
Java AIO 在 Linux 上没有真异步内核,效率不如 NIO;在 Windows IOCP 上才是真异步
🎤 面经
[蚂蚁]R1 #6665 NIO 和 BIO 的区别;NIO 的类库或框架(Netty)
答:BIO 同步阻塞一线程一连接;NIO 同步非阻塞 Selector 多路复用;Netty 基于 NIO
[快手]R2 #6321 nio 和 bio 的区别
答:阻塞模式不同;BIO 一连接一线程;NIO 一线程多连接靠 epoll
[网易]R1 #3172 NIO、BIO、AIO 的区别和使用场景(HTTP 默认是 NIO 还是 BIO)
答:BIO 同步阻塞、NIO 同步非阻塞、AIO 异步非阻塞;HTTP 服务一般 NIO
[小米]R1 #2117 Java IO/NIO 的理解,Selector/Channel/Buffer 是什么?NIO 如何非阻塞?
答:Channel 通道、Buffer 缓冲区、Selector 多路复用器;底层 epoll 事件驱动
[蚂蚁]R1 #6853 NIO 的过程,过程中使用的东西都是什么?
答:Channel 注册到 Selector 关心事件 → Selector.select 阻塞拿到就绪 → Buffer 读写
[字节]R1 #4801 聊一下 java nio
答:Channel+Buffer+Selector;非阻塞读写;底层 epoll 多路复用;Netty 基础
[得物]R1 #3686 BIO/NIO 的区别,Java 是如何实现 NIO
答:BIO 阻塞 read 直到数据到;NIO Channel 注册 Selector 事件就绪才操作
design-patterns-core
🎯 骨架
★ 单例模式:DCL(双重检查锁)+ volatile 防指令重排;推荐枚举(天然单例 + 反序列化安全)
★ 工厂模式:Spring BeanFactory 就是;解耦"创建对象"和"使用对象"
★ 代理模式:Spring AOP;运行时增强目标方法(日志/事务/权限)
策略模式:把算法封装成可互换的策略对象,避免 if-else 链(订单促销、支付方式)
责任链模式:处理者依次链式处理(Tomcat Filter、Spring Interceptor)
观察者模式:Spring ApplicationEvent;状态变化通知所有观察者
装饰器模式:JDK IO 的 BufferedReader/InputStream 嵌套;动态扩展功能不改原类
⚠️ 易混淆
DCL 单例必须用 volatile:对象创建分三步,重排序可能拿到半初始化对象
装饰器 vs 代理:装饰器主目的是扩展功能(多层包装);代理主目的是控制访问
策略模式 vs 责任链:策略只选一个执行;责任链每个都有机会处理
工厂方法 vs 抽象工厂:前者一个产品族;后者多个产品族
🎤 面经
[拼多多]R3 #6925 写一个单例模式,延迟加载,支持多线程访问
答:DCL+volatile:双重检查锁,volatile 防对象创建指令重排
[蚂蚁]R1 #6901 设计模式,观察者模式讲一下,几种设计模式的应用场景
答:观察者:Subject 维护 Observer 列表,状态变化时遍历通知;Spring 事件机制
[蚂蚁]R1 #6852 装饰器模式和代理模式的区别;JDK 中哪里用到?
答:装饰器扩展功能;代理控制访问;JDK IO 是装饰器;Proxy 是代理
[蚂蚁]R1 #6715 责任链模式用之前后区别;和策略模式、装饰者模式的区别
答:责任链每个有机会处理;策略只选一个执行;装饰器层层包装
[百度]R2 #7196 责任链模式和策略模式的区别?单例饿汉和懒汉的区别?
答:责任链多人处理;策略单人执行;饿汉类加载就创建(线程安全),懒汉首次用才创建
[B站]R1 #2765 设计模式,说一下策略模式和装饰器模式
答:策略:算法可互换,避免 if-else;装饰器:动态扩展功能,多层包装
[网易]R3 #3193 单例模式都有什么,是否线程安全,怎么改进
答:饿汉/懒汉/DCL/静态内部类/枚举;推荐 DCL+volatile 或枚举
[小米]R1 #3637 单例模式的几种写法,解释为什么
答:饿汉简单+类加载安全;懒汉要加锁;DCL 性能好但需 volatile;枚举最优
JVM
S1 JVM 内存世界
jvm-memory-structure
🎯 骨架
线程私有 3 块:程序计数器(唯一不 OOM)、虚拟机栈(栈帧 = 局部变量表+操作数栈+动态链接+返回地址)、本地方法栈(Native 方法)
线程共享 2 块:堆(对象实例,GC 主战场)、方法区(类元数据 + 运行时常量池,JDK 8+ 实现为元空间)
直接内存:不属于 JVM 规范,NIO DirectByteBuffer 用,也可能 OOM
引用在栈,对象在堆:new 出来的对象在堆上,引用变量在虚拟机栈局部变量表
字符串常量池:JDK 7 已从方法区移到堆(不是 JDK 8)
元空间使用本地内存,与 GC 解耦,默认无上限,生产建议 -XX:MaxMetaspaceSize 兜底
程序计数器只存当前字节码地址,大小固定极小,不会 OOM
⚠️ 易混淆
方法区是规范、永久代/元空间是实现,三者别混用
字符串常量池移到堆是 JDK 7(不是 JDK 8)
IO 等待在 JVM 层面是 RUNNABLE,但实际在等内核
static HashMap 放在元空间持有引用、对象实例本身仍在堆上
🎤 面经
[蚂蚁]R1 #6849 JVM 内存模型,新生代和老年代之间有什么关系?什么时候对象会放进老年代?
答:5 大区+直接内存;触发晋升 4 种:年龄 ≥15、动态年龄判断、Survivor 不下、大对象阈值
[阿里]R2 #46 jvm 内存结构(追问:方法区存储什么数据)
答:线程私有 PC/栈/本地栈,共享堆+方法区;方法区存类元数据+运行时常量池+静态变量
[京东]R1 #3088 运行时数据区域(内存模型)
答:PC、虚拟机栈、本地方法栈(线程私有),堆、方法区(线程共享),堆外有直接内存
[快手]R2 #6326 JVM 分成哪几块,运行时数据区有哪几部分?本地方法区里放的是什么?
答:5 大区;本地方法栈存 Native 方法栈帧(不是常量),方法区才存类信息
[滴滴]R1 #3018 Java 运行时有哪些区域,分别做什么?项目中哪些场景用到?
答:DTO 在堆、栈帧弹出局部变量、ThreadLocal 用 Thread.threadLocals、动态代理类入元空间
[滴滴]R3 #2575 jvm 运行时数据区介绍。java 中如何直接访问内存
答:5 大区;DirectByteBuffer + Unsafe.allocateMemory 走堆外内存,不被 JVM 管理
[网易]R1 #5565 Java 运行时区域中哪些是线程共享的哪些是线程私有的
答:私有 PC/虚拟机栈/本地栈;共享 堆/方法区;堆外直接内存也共享
[百度]R2 #5651 jvm 内存模型
答:堆+方法区共享,PC/栈/本地栈私有;JDK8 元空间放本地内存;常量池在堆
heap-generation
⚠️ 易混淆
Eden:S0:S1=8:1:1 是 CMS/Parallel 的固定比例,G1 下 Eden Region 数量动态调整
ZGC 平均 RT 反而略高(读屏障),换来的是 P99 稳定,不是"RT 更低"
CMS 只管老年代搭档 ParNew;G1 年轻代老年代都管
Humongous Region 属于老年代,Young GC 不回收,要等 Mixed GC
🎤 面经
[腾讯/京东][R1] 为什么堆要分 young 和 old?
答:弱分代假说:98% 对象朝生夕死。新生代存活率极低用复制算法(只复制 ~2%),老年代存活率高用标记-压缩。分代是为了针对不同存活率选最优算法。
[字节][R1] 新生代和老年代占比如何设置?为什么?
答:CMS/Parallel 默认 1:2(-XX:NewRatio=2),Eden:S0:S1=8:1:1。G1 下没有固定比例,Eden Region 数量由 JVM 根据 MaxGCPauseMillis 动态调整,不需要手动设置。
[蚂蚁][R3] Minor GC、Major GC、Full GC 区别?什么场景触发 Full GC?
答:Minor GC 回收新生代,频繁但快。Major GC 通常指老年代 GC(CMS 并发标记周期)。Full GC 回收整堆+元空间,STW 最长。
[携程][R1] CMS 和 G1 的区别?
答:三点:①算法:CMS 标记-清除不压缩,碎片化退化 Full GC;G1 拷贝式无碎片。②负责范围:CMS 只管老年代搭档 ParNew;G1 年轻代老年代都管。③布局:CMS 固定分代;G1 Region 化可预测停顿(MaxGCPauseMillis 预算内贪心选垃圾最多的 Region)。
[滴滴][R1] G1 特点与可预测停顿?
答:G1 把堆切成 ~2048 个 Region,动态扮演 Eden/Survivor/Old/Humongous 角色。可预测停顿:追踪每个 Region 回收价值,在 MaxGCPauseMillis 预算内贪心选垃圾最多的 Region,Mixed GC 连续多轮直到垃圾比例 < 5%。
[阿里][R2] Xms 和 Xmx 设置策略?
答:设成一样大。堆动态扩容时 JVM 向 OS 申请内存会触发 Full GC 并 STW,设成一样大启动时就分配好全部内存,避免运行期扩容停顿。
[网易][R1] 频繁 Full GC 怎么排查?
答:看 GC 日志找原因:CMS 看是否 CMF/Promotion Failed(碎片化);G1 看是否 To-space Exhausted(大对象/内存泄漏/分配速率过高);ZGC 看 Allocation Stall 频率;通用看元空间是否溢出。再结合 jmap histogram 对比快照排查泄漏,火焰图定位大对象分配热点。
object-creation
🎯 骨架
类加载检查:检查类是否已加载,没有则触发类加载
分配内存:指针碰撞(堆规整)或空闲列表(堆不规整);TLAB 线程本地分配避免竞争
初始化零值:所有字段赋默认值(int=0, ref=null)
设置对象头:Mark Word(哈希码/GC年龄/锁状态)+ 类型指针
执行 ``:按代码赋初始值 + 构造方法
⚠️ 易混淆
步骤 3 是零值初始化(JVM 做),步骤 5 是代码初始化(程序员写的)
TLAB(Thread Local Allocation Buffer):每个线程在 Eden 区预分配一块,避免多线程分配时的 CAS 竞争
🎤 面经
[滴滴 3面] Java 对象创建的完整流程是什么?→ 类加载检查→分配内存(TLAB/指针碰撞)→零值初始化→设置对象头→执行 init
[字节 2面] Java 多个线程同时 new 对象存在竞争的情况会发生什么?→ 用 TLAB 避免竞争,TLAB 不够时 CAS 在 Eden 分配
[Shopee 2面] 描述一个 Java 对象从创建到被垃圾回收的完整生命周期?→ new→Eden→Survivor(Minor GC 晋升)→Old(年龄/大对象)→GC 回收
[小米 1面] 除了 new 还有哪些方式创建对象?区别?→ 反射/clone/反序列化/Unsafe.allocateInstance;clone 不调构造方法,反序列化也不调
[网易 1面] 如何创建一个 Java 对象?→ new/反射/clone/反序列化,各自触发 init 的情况不同
object-memory-layout
🎯 骨架
★ 三部分:对象头(Mark Word + Class Pointer)+ 实例数据 + 对齐填充(补齐 8 字节整数倍)
★ Mark Word(64位):复用设计——无锁存 hashcode+age,偏向锁存 threadId,轻量级锁存栈帧指针,重量级锁存 Monitor 指针
★ synchronized 本质:修改对象头 Mark Word 的锁标志位(00=轻量级,10=重量级,01=无锁/偏向)
Class Pointer:指向类元数据,开启压缩后 4 字节(默认开启,堆 ≤ 32GB 时有效)
对齐填充原因:CPU 缓存行对齐 + 指针压缩需要 8 字节对齐
new Object() 最小 16 字节:Mark Word(8) + Class Pointer(4) + 对齐填充(4)
⚠️ 易混淆
Mark Word 是复用的,同一 64 位在不同锁状态下含义完全不同
数组对象多一个 4 字节数组长度字段
指针压缩失效条件:堆 > 32GB(2^35 × 8 = 32GB 寻址上限)
🎤 面经
[字节][R3] new 一个对象会用到哪些内存?→ 对象头(Mark Word+Class Pointer) + 实例数据 + 对齐填充,最小 16B
[字节][R3] int a = 1 会用到哪些内存?→ 局部变量在虚拟机栈局部变量表,4 字节
[通用][R1] 对象头里存了什么?→ Mark Word(锁状态/hashcode/GC年龄)+ Class Pointer(类型指针)
[通用][R1] synchronized 底层是怎么加锁的?→ 修改对象头 Mark Word 的锁标志位
[通用][R1] 为什么对象要 8 字节对齐?→ CPU 缓存行对齐 + 指针压缩需要低 3 位为 0
permgen-vs-metaspace
🎯 骨架
永久代(JDK 7 及之前):方法区的 HotSpot 实现,在 JVM 堆内,大小固定(-XX:MaxPermSize),容易 OOM
元空间(JDK 8+):方法区的新实现,在本地内存(Native Memory),不受 JVM 堆大小限制,默认无上限
存储内容:类元数据(类名/方法/字段/字节码)、运行时常量池、静态变量(JDK 7 起移到堆)
为什么换:永久代大小难以预估,频繁 Full GC;元空间用本地内存,动态扩展,减少 OOM 风险
String 常量池:JDK 7 从永久代移到堆,JDK 8 元空间里没有 String 常量池
⚠️ 易混淆
元空间不在 JVM 堆里,在本地内存(OS 管理),不受 -Xmx 限制
元空间仍然可以 OOM(本地内存耗尽,或设置了 -XX:MaxMetaspaceSize)
方法区是规范,永久代/元空间是实现
🎤 面经
[蚂蚁 R1] MetaSpace 从 JDK 哪个版本开始?和 1.7 永久代有什么区别?为什么要换成 MetaSpace?→ JDK 8,本地内存替代堆内固定大小,减少 OOM
[阿里 R1] JVM 的方法区和元空间有啥关系?元空间是完全替代方法区了吗?→ 元空间是方法区的 HotSpot 实现,方法区是规范
[携程 R1] JDK 1.7 和 1.8 JVM 有什么区别?→ 永久代→元空间,String 常量池移到堆
[小米 R1] Perm Space 保存什么?会 OOM 吗?1.8 后 MetaSpace 变动?→ 类元数据/常量池,会 OOM,1.8 改用本地内存
[字节 R2] 方法区主要存储些什么?方法区规范是一直存在的吗?→ 类元数据/运行时常量池/静态变量,规范一直在,实现变了
S2 GC 算法与回收器全景
gc-algorithms
🎯 骨架
标记-清除:标记存活对象,清除未标记对象。缺点:内存碎片
标记-整理:标记后将存活对象移到一端,消除碎片。缺点:移动对象开销大
复制算法:将存活对象复制到另一块空间,清空原空间。缺点:空间利用率 50%
分代收集:Young 区用复制(存活少),Old 区用标记-清除/整理
⚠️ 易混淆
CMS 用标记-清除(有碎片),G1 用复制+标记整理(无碎片)
G1 不是完全并发,Young GC 仍有 STW;ZGC 几乎全并发
🎤 面经
[美团 1面] 垃圾回收算法?CMS 为什么用标记清除?→ 标记清除不移动对象,并发标记时对象地址不变,适合低停顿场景;代价是碎片
[字节 1面] 了解哪些垃圾回收器?区别?→ CMS低停顿有碎片;G1可预测停顿无碎片;ZGC几乎无停顿
[携程 1面] CMS 针对什么场景?具体流程?→ 低延迟场景;初始标记(STW)→并发标记→重新标记(STW)→并发清除
[携程 1面] G1 的适用场景?→ 大堆(4GB+)+ 可预测停顿;Region 化,混合回收 Young+Old
[滴滴 1面] G1 如何实现可预测停顿?→ 维护每个 Region 的回收价值,优先回收价值高的 Region,控制 GC 时间
[字节 2面] G1 和 CMS 的 GC 过程分别是怎样的?→ CMS:初始标记→并发标记→重新标记→并发清除;G1:Young GC + Mixed GC
gc-roots-reachability
🎯 骨架
判活方式:引用计数法(无法解决循环引用,JVM 不用)vs 可达性分析(从 GC Roots 出发遍历,遍不到的就是垃圾)
GC Roots 四大类:
虚拟机栈(栈帧局部变量表)中引用的对象
方法区静态变量引用的对象(static Foo f = new Foo())
方法区常量池引用的对象(String s = "abc")
本地方法栈 JNI 引用的对象
补充类:被同步锁 synchronized 持有的对象、JVM 内部引用(Class 对象、异常类)、线程对象本身
跨代引用:Old → Young 跨代引用通过 Card Table(CMS)/ RSet(G1)记录,避免每次 Young GC 都扫描整个老年代
OopMap:JIT 编译时记录每个安全点的引用位置,扫 GC Roots 时不用一字一字找,O(1) 定位
安全点(Safepoint):所有线程暂停在安全点(方法调用/循环回边/异常处)才能保证 OopMap 准确
选 GC Root 的标准:生命周期长 + 数量少 + 可枚举
⚠️ 易混淆
栈中"基本类型变量"不是 GC Root(不是引用)
引用计数能不能解决循环引用:单纯不行,但加 + 弱引用打破环就行(Python 用的)
跨代引用占比 < 1%(弱分代假说推论),所以 RSet/Card Table 性能开销可接受
选取标准:栈帧瞬时变化但数量有限 + 静态变量长期存活 → 都是好 Root
🎤 面经
[快手]R1 #2672 什么样的对象叫根对象
答:栈帧局部变量、静态变量、常量池引用、JNI 引用 + 同步锁/JVM 内部对象
[京东]R4 #1944 cms;增量更新法;GcRoots 是哪些
答:栈帧/静态/常量池/JNI 四类;CMS 用增量更新写屏障,黑色对象新增引用时重标灰
[阿里]R3 #254 jvm 怎么回收垃圾,引用计数和根可达分析,jvm 怎么确定根元素
答:可达性分析 + OopMap + 安全点;引用计数 JVM 不用,循环引用问题
[拼多多]R5 #944 Java GC 机制?GC Roots 有哪些?
答:可达性分析;4 类 Root;标记清除/复制/标记整理/分代
[字节]R1 #347 JVM、gc root,为什么要有 gc root?
答:从已知活的根开始遍历,找不到的判垃圾;引用计数解决不了循环引用所以用根可达
[阿里]R4 #6613 GCRoot 对象有哪些 选取 GCRoot 的标准是什么
答:4 类;选取标准是生命周期长 + 数量少 + 可枚举(栈帧+静态变量天然满足)
[快手]R1 #2641 GC Roots 有哪些类型
答:栈帧局部变量、方法区静态、常量池引用、本地方法栈 JNI;补充同步锁/Class 对象
[小红书]R2 #7293 什么东西可以当做 GC Root,跨代引用怎么办?
答:4 类;跨代用 Card Table(CMS)/ RSet(G1)记录 Old→Young,避免扫整个老年代
generation-model
🎯 骨架
理论基础:弱分代假说(98% 对象朝生夕死)+ 强分代假说(老对象往往继续老下去)→ 不同存活率选不同算法
新生代 = Eden : S0 : S1 = 8 : 1 : 1(CMS/Parallel 固定,G1 不固定)
新生代用复制算法:存活率仅 ~2%,只复制 2% 极便宜,无碎片
老年代用标记-整理 / 标记-清除:存活率高,复制代价大
新生代默认 1/3 堆,老年代 2/3(-XX:NewRatio=2),可调
三种 GC 类型:
Minor GC(Young GC):回收新生代,频繁但快(10-50ms)
Major GC:通常指老年代 GC(CMS 并发标记)
Full GC:回收整堆 + 元空间,STW 最长
G1 颠覆:物理上 Region 化,Eden/Survivor/Old 是 Region 角色标签,不是固定区域
⚠️ 易混淆
Major GC 和 Full GC 不是一回事,前者只清老年代、后者整堆+元空间
G1 没有"固定的 Eden 占比",Region 数量动态调
弱分代假说不是 100%,所以仍需要跨代引用机制(Card Table/RSet)
ZGC(JDK 15-20)一度无分代,JDK 21 分代 ZGC 重新引入 Young/Old
🎤 面经
[蚂蚁]R1 #6629 minorGC、majorGC、fullGC 的区别及触发场景
答:Minor 回收新生代,Major 回收老年代,Full 整堆+元空间;Eden 满触发 Minor,老年代满或元空间溢出触发 Full
[京东]R1 #5497 堆空间的结构?分配策略有哪些?Minor GC 和 Full GC 的区别?
答:Eden:S0:S1=8:1:1+Old;优先 Eden→TLAB→大对象进 Old;Minor 频快,Full 整堆 STW 长
[字节]R4 #641 Minor GC 会清空 eden 吗?太大的对象放不进年轻代怎么办?
答:Eden+S0 存活拷到 S1,Eden 清空;大对象超 PretenureSizeThreshold 直接进老年代
[拼多多]R3 #1269 JVM 自动内存管理,Minor GC 与 Full GC 的触发机制
答:Eden 满 → Minor;老年代占用超阈值/晋升失败/碎片化/元空间溢出/System.gc() → Full
[美团]R4 #3976 JVM 中的内存区域划分,堆的分代,为什么分代,垃圾回收算法,垃圾回收器
答:弱分代假说,新生代复制(存活少)老年代标记整理(存活多);选不同算法
[京东]R1 #3999 年轻代和老年代各用什么垃圾收集算法?为什么需要两个 Survivor?
答:新生代复制(Eden+S0→S1),老年代标记清除/整理;两个 S 是为了复制时无碎片+方便清理
[网易]R1 #1743 GC 为什么分代?什么情况直接分配老年代?频繁 FGC 排查?
答:分代针对存活率选最优算法;大对象/超龄/Survivor 不下;看 GC 日志找 CMF/碎片/元空间
[京东]R3 #2042 堆为什么要分成新生代、老年代?比例为什么是 1:2?
答:弱分代假说;1:2 是默认(NewRatio=2),经验值平衡晋升频率和老年代寿命,可调
object-promotion
🎯 骨架
晋升 4 种触发(CMS/Parallel 固定分代):
年龄阈值:Survivor 中熬过 15 次 Minor GC(-XX:MaxTenuringThreshold=15)
大对象直接进老年代:超 -XX:PretenureSizeThreshold 的对象绕过新生代
Survivor 空间不足:Minor GC 后 Survivor 放不下,直接晋升
动态年龄判断:Survivor 中相同年龄对象总大小 > Survivor 50%,≥该年龄全部晋升(无需等到 15)
G1 差异:年龄阈值动态(按停顿目标调),大对象阈值是 Region/2(约 2MB)→ 进 Humongous Region(属老年代)
晋升过程:Eden + S0 存活对象 → S1(年龄+1)→ 满足晋升条件 → 直接拷贝到 Old Region
TLAB 影响:分配仍走 Eden,TLAB 是 Eden 内的线程私有缓冲,不影响晋升判断
Survivor From/To 角色每次互换("半区复制")
晋升失败 = Promotion Failed:老年代碎片化没连续空间 → 退化 Full GC(CMS 经典坑)
大对象先生型:分配时直接走老年代,绕过 Eden,避免 Eden 频繁 Minor GC 拷贝大对象的开销
⚠️ 易混淆
"晋升年龄阈值 = 15" 是默认值,可能因动态年龄判断提前
G1 下大对象阈值是 Region/2 不是固定 PretenureSizeThreshold
动态年龄判断 ≥ 50% 这个值(不是 > 50%)
Humongous Region 算老年代,Young GC 不回收(JDK 8u60+ 优化可顺带回收无引用 H 区)
🎤 面经
[阿里]R4 #6615 年轻代 GC 一定次数进入老年代,还有什么会进入老年代
答:3 种额外路径——大对象直接进、Survivor 不下、动态年龄判断(同龄总和 ≥50%)
[阿里]R1 #94 什么情况下对象会回收到老年代?
答:年龄 ≥15、大对象(PretenureSizeThreshold/Humongous)、Survivor 不下、动态年龄判断
[小米]R2 #1911 做 GC 时,一个对象在内存各个 Space 中被移动的顺序是什么?
答:Eden → S0/S1(互换)→ Old;每次 Minor 年龄 +1,达阈值或动态年龄 → 晋升 Old
[小米]R1 #2093 为什么新生代要被分为 Eden、From Survive、To Survive 三个区?
答:复制算法需要 from/to 两块腾挪;Eden 用 8 大头(98% 朝生夕死),S 各 1 留拷贝
[字节]R4 #641 Minor GC 会清空 eden 吗?太大的对象放不进年轻代怎么办?
答:清空(存活拷到 S1);大对象超 PretenureSizeThreshold 直接进老年代避免反复拷贝
[快手]R1 #2672 新生代如何进入老年代(3 种情况)?
答:年龄 ≥15、大对象、Survivor 不下;补充动态年龄判断
[蚂蚁]R1 #6849 什么时候对象会放进老年代?static HashMap 放在哪个代?ThreadLocal 的 HashMap 呢?
答:4 种触发;static 由静态变量持有 → 长期存活 → 最终晋升 Old;ThreadLocal Map 同理
[网易]R1 #1743 什么情况直接分配到老年代?
答:超 PretenureSizeThreshold(CMS)/ Region/2(G1,进 Humongous)的大对象绕过 Eden
fullgc-triggers
🎯 骨架
通用 5 类(所有收集器都有):
元空间不足(Metaspace OOM)→ 类加载泄漏(Aviator/动态代理)
System.gc() 显式调用 → DirectByteBuffer/堆外内存清理常见
大对象分配失败 → 堆没连续空间
CMS 兜底/G1 兜底 → 各自的并发回收跟不上
担保失败 → 老年代担保不了 Young GC 晋升
CMS 特有:
Concurrent Mode Failure:并发期间老年代满了,CMS 来不及 → 退化 Serial Old
Promotion Failed:晋升时老年代碎片化,没连续空间
G1 特有:
To-space Exhausted:Free Region 耗尽,Mixed GC 跟不上分配速度
Humongous 分配失败:没足够连续 Region
ZGC 特有(JDK 15+ 基本消除 Full GC):极端情况退化为 Allocation Stall(阻塞分配线程,不是传统 Full GC)
Full GC 后果:单线程整堆压缩(CMS 退化用 Serial Old),STW 长达数秒
JDK 10+ G1 Full GC 改多线程:从 ~3s → 几百 ms
System.gc() 屏蔽:-XX:+DisableExplicitGC 关掉显式 GC(注意会让 DirectBuffer 内存释放滞后)
⚠️ 易混淆
Major GC ≠ Full GC(前者只清老年代,后者整堆+元空间)
Concurrent Mode Failure 和 Promotion Failed 都是 CMS 退化路径,但触发者不同(前者 CMS 自己、后者 ParNew Young GC)
G1 的 Humongous 阈值是 Region/2 不是固定值
JDK 8 的 G1 Full GC 是单线程,JDK 10+ 才并行(重要!)
🎤 面经
[快手]R2 #6378 什么时候进行 Full GC 呢?
答:通用 5 类(Metaspace/System.gc/大对象/担保失败/兜底)+ CMS 的 CMF/Promotion Failed + G1 的 To-space Exhausted
[蚂蚁]R1 #6629 minorGC、majorGC、fullGC 的区别及触发场景
答:Minor 新生代频快;Major 通常指老年代;Full 整堆+元空间,触发=老年代满/CMF/元空间溢出/System.gc
[京东]R1 #5471 g1 什么场景触发 full gc?
答:To-space Exhausted(Free Region 耗尽)/ Humongous 失败 / 元空间溢出 / System.gc()
[字节]R4 #641 minor GC 和 full GC 的区别,太大的对象放不进年轻代怎么办?
答:Minor 新生代复制频快;Full 整堆 STW 长;大对象超阈值直接进老年代避免反复拷贝
[拼多多]R3 #1269 Minor GC 与 Full GC 的触发机制
答:Eden 满 → Minor;老年代占用超阈值/晋升失败/碎片化/元空间溢出/System.gc → Full
[阿里]R3 #110 minorGC、majorGC、fullGC 的区别,什么场景触发 fullGC
答:CMS:CMF/Promotion Failed;G1:To-space Exhausted/Humongous;通用:元空间/System.gc
[网易]R1 #1743 频繁 FGC 怎么排查?什么情况直接分配到老年代?
答:先看 GC 日志找原因;大对象 Pretenure/G1 Humongous 直接进老年代
[快手]R2 #2813 什么时候进行 Full GC 呢?
答:CMS CMF/Promotion Failed;G1 To-space Exhausted;元空间溢出;System.gc()
reference-types
🎯 骨架
四种引用强度递减:强 > 软 > 弱 > 虚
强引用(Strong):Object o = new Object(),永不回收,只要可达就活,OOM 也不回收
软引用(SoftReference):内存不够才回收(Young GC 不动;Full GC 时若仍不够则回收),适合缓存
弱引用(WeakReference):下一次 GC 必回收(不管内存够不够),典型用法 ThreadLocalMap 的 Entry.Key、WeakHashMap
虚引用(PhantomReference):唯一作用是对象被回收时收到通知(ReferenceQueue),DirectByteBuffer 用它通知释放堆外内存
ReferenceQueue:软/弱/虚都可以注册队列,对象被回收前 Reference 入队,可做清理回调
ThreadLocal 泄漏:Key 是弱引用(GC 后变 null),Value 是强引用(线程不死就一直在)→ 必须 remove()
⚠️ 易混淆
软引用 ≠ 内存吃紧就回收,是Full GC 时若回收完仍不够内存才回收软引用
WeakHashMap 的 Key 是弱引用,但 Value 是强引用(同 ThreadLocal)
String.intern() 返回的是常量池里的强引用,不是弱引用
虚引用必须配合 ReferenceQueue 使用,单用没意义
🎤 面经
[蚂蚁]R4 #4173 集合类如何解决(内存)问题?软引用和弱引用的区别?
答:软引用内存不够才回收(缓存);弱引用下次 GC 必回收(WeakHashMap、ThreadLocal Key)
[滴滴]R2 #2504 拷打 JVM 基础概念,为啥 ThreadLocal 内存泄漏
答:Key 弱引用 GC 后变 null,Value 强引用线程池不销毁就一直在,必须 remove
[字节]R2 #325 你说的内存泄漏具体是怎么产生的?线程池的线程是不是必须手动 remove 才可以回收 value?
答:弱 Key 被回收变 null Entry,Value 强引用挂在 Thread.threadLocals;线程池线程不死必须手动 remove
[阿里]R1 #77 ThreadLocal 的实现原理靠什么?知道可能导致内存泄漏的原因后,具体怎么防范?
答:每个 Thread 有 ThreadLocalMap,Key 弱 Value 强;try-finally 显式 remove;用 InheritableThreadLocal 跨线程
[得物]R1 #2687 JVM 是如何避免内存泄漏的?
答:可达性分析自动回收无引用对象;软/弱/虚引用控制对象生命周期;finalize 兜底(不推荐)
[小米]R1 #2075 哪些对象可以作为 GC ROOT?如何排查内存泄漏?
答:4 类 Root;jmap dump 后 MAT 看 Dominator Tree + Leak Suspects 找 GC Root 引用链
[蚂蚁]R4 #4172 Java 栈什么时候会发生内存溢出,Java 堆呢,说一种场景
答:栈深度 StackOverflowError(递归无终止);堆 OOM(集合持有对象不释放,引用强不可回收)
[字节]R3 #3335 内存溢出和内存泄漏?什么情况下会出现?怎么避免?
答:泄漏 = 不该活的还活着(强引用未释放);溢出 = 真的不够;改弱引用、显式置 null、缓存设过期
cms-collector
🎯 骨架
定位:第一个并发 GC(JDK 1.4.1,2002),目标低停顿;JDK 9 标记废弃,JDK 14 移除
算法:标记-清除(不压缩 → 碎片化是其原罪)
只管老年代,新生代由 ParNew 搭档(多线程复制)
完整 7 阶段(只 2 次 STW):
初始标记(STW,标 Roots 直接引用)
并发标记(最耗时,遍历对象图)
并发预清理(扫 Dirty Card 减重标压力)
可中断预清理(等一次 Young GC 清空年轻代,超时 5s)
重新标记(STW,处理增量更新,CMS 最长停顿)
并发清除(清死亡对象,不压缩)
并发重置(准备下一轮)
致命缺陷链:标记清除不压缩 → 碎片化 → Promotion Failed(晋升找不到连续空间)/ Concurrent Mode Failure(并发期间老年代满)→ 退化 Serial Old Full GC(单线程整堆压缩,灾难级)
关键参数:
CMSInitiatingOccupancyFraction=92(默认,老年代到 92% 才触发,常调到 70-75 留缓冲)
CMSScavengeBeforeRemark:Final Remark 前先做一次 Young GC 缩短停顿
UseCMSCompactAtFullGC:Full GC 时启用整理(CMS 默认不整理)
浮动垃圾:并发期间应用断开的引用本轮不回收 → 留给下一轮 → 必须留缓冲(这就是为什么不能等老年代满才触发)
⚠️ 易混淆
CMS 用增量更新(不是 SATB),写屏障在新增引用时触发
只两次 STW 但重新标记是最长停顿(不是初始标记)
退化用的是 Serial Old(单线程),不是 Parallel Old,所以才那么慢
CMS 不能压缩 → 用空闲列表分配(不是指针碰撞)
🎤 面经
[百度]R2 #7201 介绍下 cms 收集器
答:老年代收集器,标记清除算法,4 大步初始标记→并发标记→重新标记→并发清除(实际 7 阶段)
[腾讯]R3 #3136 cms 算法的缺点;标记清除算法的过程;如何给对象分配内存空间?
答:碎片化退化 Full GC;标活的不动死的清掉;空闲列表分配(不是指针碰撞)
[携程]R1 #5271 CMS 针对什么场景,采用什么算法,具体流程,是否需要配合其他垃圾回收器
答:低延迟场景;标记清除;4 大步只 2 次 STW;必须配合 ParNew 处理新生代
[快手]R1 #2746 为什么你的项目里用的是 CMS 收集器,说下 G1 和 CMS 收集器的区别
答:JDK 8 默认 Parallel/CMS 可选,CMS 低延迟;G1 拷贝式无碎片可预测停顿,CMS 标记清除有碎片
[蚂蚁]R1 #6748 说说 GC 的过程。CMS GC 有什么问题?怎么避免产生浮动垃圾?
答:4 大步;碎片化 + CMF + 浮动垃圾;浮动垃圾设计代价,可缩窗口降阈值
[蚂蚁]R4 #4171 CMS 回收停顿了几次?为什么要停顿两次?
答:2 次(初始标记+重新标记);初始标记保证 Root 一致性;重新标记修正并发期间引用变化
[百度][R1] CMS 优化做过吗?
答:CMSInitiatingOccupancyFraction 降到 70-75% 留缓冲;+CMSScavengeBeforeRemark 缩短重标停顿
[其他]R1 #4369 性能优化做过吗?arthas,CMS 优化
答:升 G1 是治本;不能升就调 CMSInitiatingOccupancyFraction + CMSScavengeBeforeRemark
g1-collector
🎯 骨架
定位:Region 化分代 + 可预测停顿(JDK 7u4 可用,JDK 9 默认)
Region 化:堆切成 ~2048 个 Region(1-32MB),动态扮演 Eden/Survivor/Old/Humongous/Free
三种 GC 模式:
Young GC:回收全部 Eden + Survivor,存活拷贝到新 Region,全程 STW
Mixed GC:Young + 选择性 Old,按垃圾比例排序,MaxGCPauseMillis 预算内贪心选
Full GC:兜底(JDK 10+ 多线程整堆压缩)
可预测停顿:-XX:MaxGCPauseMillis=200(默认),追踪每个 Region 回收价值,预算内贪心,分多轮 Mixed GC 直到老年代垃圾比例 < 5%
RSet(Remembered Set):每个 Region 记录"谁引用了我"(Old→Young 跨代/Old→Old 跨 Region),写屏障维护,占堆 5-10%
并发标记 5 阶段:初始标记(搭便车 Young GC)→ Root Region Scan → 并发标记(SATB)→ 重新标记(STW)→ Cleanup
触发链:老年代占用 > IHOP(默认 45%)→ 启动并发标记周期 → 完成后下次 GC 变 Mixed GC → 连续多轮直到达标
⚠️ 易混淆
Humongous(≥ Region/2 的对象)独占连续 Region,逻辑属老年代,不走 Eden
可预测停顿 ≠ 一定满足,分配速率超过回收速率 → To-space Exhausted → Full GC
初始标记是"搭便车"在 Young GC 上,不是独立 STW,比 CMS 聪明
G1 用 SATB(不是增量更新),重新标记开销小,但浮动垃圾更多
🎤 面经
[滴滴]R3 #2982 G1 有哪些特点?G1 如何实现可预测的停顿时间?漏标问题如何解决?
答:Region+分代+拷贝;追踪每个 Region 回收价值预算内贪心;SATB+写屏障
[京东]R1 #3056 为什么用 G1 垃圾回收器?region 大小?
答:可预测停顿+大堆友好;Region 1-32MB,JVM 自动按目标 ~2048 个算
[携程]R1 #2905 G1 收集器
答:Region 化分代+拷贝式无碎片+可预测停顿;并发标记 + Mixed GC
[美团]R1 #3097 了解 JVM 的 G1 垃圾回收器吗?
答:JDK 9 默认;适合大堆+延迟敏感;MaxGCPauseMillis 控制停顿;Mixed GC 选高价值 Region
[快手]R2 #6378 你了解哪些收集器?详细谈谈 G1 的优点
答:可预测停顿+无碎片(拷贝式)+大堆友好+分代不固定(Region 动态角色)
[蚂蚁]R1 #6748 G1 回收过程是怎么样的?Remember Set 底层是怎么实现的?
答:Young GC + 并发标记 + Mixed GC;RSet 用 Hash/Card Table 记跨 Region 引用,写屏障维护
[腾讯]R1 #2194 G1 为什么叫并发低延迟?什么时候不用 G1?
答:并发标记+局部回收;小堆(<4GB)用 Parallel 吞吐更高;超大堆(32GB+)建议 ZGC
[京东]R1 #5471 g1 什么场景触发 full gc? young gc? 垃圾回收的策略?
答:Eden 满 → Young GC;老年代>IHOP → 并发标记+Mixed;To-space Exhausted/Humongous 失败 → Full GC
tri-color-marking
🎯 骨架
三色含义:
白色:未标记,扫描结束仍白色 = 垃圾
灰色:自己被标了但子引用还没扫完
黑色:自己 + 全部子引用都扫完
标记流程:所有对象初始白色 → GC Root 直接引用变灰 → 灰色对象的子节点变灰、自身变黑 → 反复直到无灰色 → 剩余白色全是垃圾
漏标两个必要条件(同时满足):
黑色对象新增引用白色对象(black.ref = white)
所有灰色对象到该白色对象的引用路径都被断开(gray.ref = null)
漏标后果:白色对象被误回收 → 程序崩溃(不能宁可漏不能错杀)
CMS 解法 = 增量更新(Incremental Update):破坏条件 1,黑色新增引用时写屏障把黑色重标灰,重新标记阶段再扫
G1 解法 = SATB(Snapshot At The Beginning):破坏条件 2,引用被删除时写屏障把旧引用入 SATB 队列,后续扫
写屏障:JVM 在引用赋值的机器码中插入的一段检查逻辑,不是字节码层面(很短,几条指令)
⚠️ 易混淆
SATB 和增量更新都用写屏障,触发时机不同(SATB 在删除引用时、增量更新在新增引用时)
SATB 浮动垃圾更多(保守保留旧引用目标),增量更新重新标记开销更大
漏标必须两条件同时满足,缺一不漏标
写屏障 ≠ Java 内存模型 happens-before 那个屏障,是 GC 专用术语
🎤 面经
[滴滴]R3 #2982 G1 如何实现可预测的停顿时间?漏标问题如何解决?介绍下三色标记?说说 STAB 算法?
答:贪心选高价值 Region;G1 用 SATB(删引用时入队),CMS 用增量更新;三色 = 黑灰白
[腾讯]R1 #7073 go 的 gc 是怎么实现的呢?三色标记大概是怎么样一个原理?每个颜色代表的是什么意义呢?
答:Go 用三色标记+混合写屏障;白未标 灰子引用没扫完 黑全扫完
[字节]R1 #7329 Go 的 GC 机制是什么?三色标记法如何工作?
答:Root → 灰 → 扫子节点 → 子变灰自变黑 → 反复直到无灰;写屏障防漏标
[小红书]R1 #5348 CMS 和 G1 均使用三色标记法进行标记。跨代引用:新生代引用老年代如何回收。g1 引入了 remember set
答:三色 + Card Table(CMS)/ RSet(G1)记跨代;Old→Young 通过 RSet 找根
[蚂蚁]R1 #6815 Remember Set 底层是怎么实现的?
答:每个 Region 维护一个 Hash 表/Card Table,写屏障在跨 Region 赋值时把发起 Region 卡号写入目标 RSet
[蚂蚁]R1 #6748 怎么避免产生浮动垃圾?
答:浮动垃圾是设计代价,没法完全避免;可以缩短并发标记窗口(提前触发 GC、加 CPU 减并发期)
[京东]R4 #1944 cms 增量更新法
答:黑色对象新增引用白色时,写屏障把黑色重新标灰,重新标记阶段再扫描
[其他][R1] 写屏障是字节码插入还是机器码?
答:JVM 在 JIT 编译生成的机器码中插入,几条机器指令很短,不是字节码层面修改
zgc-shenandoah
🎯 骨架
定位:超低延迟 GC,目标 STW < 1ms 且与堆大小无关;ZGC(Oracle)+ Shenandoah(Red Hat)几乎同期
ZGC 核心机制 = 染色指针 + 读屏障:
染色指针:指针高 4 位编码 GC 元数据(Marked0/Marked1/Remapped/Finalizable),不需要额外 Mark Word
读屏障:每次读引用时检查指针颜色,旧地址通过转发表自动修正(自愈),实现搬移与应用并发
Shenandoah 核心机制 = Brooks 转发指针 + 读屏障:每个对象前加一个转发指针,搬移后改指针指向新地址(OpenJDK,可在 JDK 12+ 用)
3 次 STW 各 < 1ms:初始标记 / 重新标记 / 初始搬移
ZGC Region 三种:Small(2MB,<256KB 对象)/ Medium(32MB,256KB-4MB)/ Large(N×2MB,>4MB 单 Region 一对象,无 Humongous 连续性问题)
JDK 时间线:JDK 11 实验、JDK 15 生产可用、JDK 16 并发栈扫描、JDK 21 分代 ZGC 生产就绪、JDK 23 Generational ZGC 默认
代价:读屏障 ~5% 吞吐开销;JDK 15 前不支持压缩指针(对象头更大);适合大堆(8GB+)低延迟场景
⚠️ 易混淆
ZGC 不是"平均 RT 更低",是"P99/P999 更稳定"——平均 RT 因读屏障反而略高
Shenandoah 在 OpenJDK,Oracle JDK 11+ 起内置 ZGC 但不带 Shenandoah
G1 的 Humongous 需要多个连续 Region,ZGC Large 单 Region 一对象,没连续性问题
JDK 21 前 ZGC 无分代(所有对象同池),不能利用弱分代假说,吞吐不如 G1
🎤 面经
[百度]R1 #2448 G1 回收器之后的回收器有了解吗(ZGC、Shenandoah)
答:ZGC 染色指针+读屏障;Shenandoah Brooks 转发指针+读屏障;目标 STW <10ms 与堆无关
[美团]R3 #4956 常用的垃圾回收器?G1 和 ZGC 原理?
答:G1 Region+可预测停顿;ZGC 染色指针+读屏障并发搬移,3 次 STW <1ms 与堆无关
[Shopee]R1 #800 ZGC 实践难点:如何解决 ZGC 在 TB 级堆内存下的停顿时间波动?
答:分代 ZGC(JDK 21)减少标记/搬移工作量;调 ZAllocationSpikeTolerance 应对突发;监控 Allocation Stall
[腾讯]R1 #2194 G1 为什么叫并发低延迟回收器?什么时候不用 G1?
答:G1 主要并发标记,搬移仍 STW;超大堆(>32GB)+ 严格延迟要求 → 换 ZGC
[其他][R1] ZGC 的吞吐为什么比 G1 低?
答:读屏障在每次引用读取时执行(颜色判断+转发表),持续消耗 CPU,吞吐降 5-10%
[其他][R1] ZGC 适合什么场景?什么场景不要用?
答:适合大堆(8GB+)+ P99 严格的在线服务;不适合追求最大吞吐的批处理(读屏障开销)
[其他][R1] JDK 21 分代 ZGC 解决什么问题?
答:之前所有对象同池不分代,Young GC 也要扫整堆;分代后利用弱分代假说降扫描量,吞吐回升
[其他][R1] 为什么 ZGC 几乎不会 Full GC?
答:并发搬移+无碎片+Allocation Stall(堆满阻塞分配等 GC)替代 Full GC,JDK 15+ 基本消除
S3 回收器调优与选型
cms-vs-g1
🎯 骨架
算法:CMS = 标记-清除(不压缩,碎片化);G1 = 拷贝式(存活搬到新 Region,原 Region 整体释放,无碎片)
负责范围:CMS 只管老年代(搭档 ParNew 处理 Young);G1 年轻代+老年代都管
内存布局:CMS 固定分代 Eden:S0:S1=8:1:1;G1 Region 化(~2048 个,动态角色)
STW 特点:CMS 两次 STW(初始标记 + 重新标记,最长在重新标记);G1 多次但每次可控(MaxGCPauseMillis 预算)
碎片:CMS 有,碎片化触发 Promotion Failed/CMF → 退化 Serial Old Full GC;G1 无碎片
漏标解法:CMS 增量更新(写屏障在新增引用时);G1 SATB(写屏障在删除引用时)
JDK 状态:CMS JDK 9 标记废弃 / JDK 14 移除;G1 JDK 9 起默认,至 JDK 21 仍主力
⚠️ 易混淆
CMS 退化用单线程 Serial Old(不是 Parallel Old),所以才特别慢
G1 的 Mixed GC 不是"一次清完老年代",是连续多轮直到垃圾比例 < 5%
G1 RSet 占堆 5-10% 内存,CMS 只用 Card Table 占用更小
小堆(< 4GB)G1 优势不明显,用 Parallel 吞吐更高
🎤 面经
[快手]R1 #2746 说下 G1 和 CMS 收集器的区别
答:算法(标记清除 vs 拷贝)/ 范围(老年代 vs 全堆)/ 布局(固定分代 vs Region);G1 可预测无碎片
[快手]R2 #6378 你了解哪些收集器?CMS 和 G1。详细谈谈 G1 的优点
答:CMS 标记清除有碎片不可预测;G1 拷贝无碎片+MaxGCPauseMillis 可控+大堆友好
[携程]R3 #2920 垃圾回收算法:CMS、G1
答:CMS 标记清除并发低停顿;G1 Region 化拷贝可预测停顿
[蚂蚁]R4 #4171 CMS 和 G1 了解么,CMS 解决什么问题,说一下回收的过程
答:CMS 解决低延迟问题但碎片化;G1 兼顾延迟+无碎片+可预测;CMS 4 大步 2 次 STW
[蚂蚁]R1 #6815 CMS GC 有什么问题?G1 回收过程是怎么样的?
答:CMS 碎片化+CMF+浮动垃圾;G1 = Young GC + 并发标记周期 + Mixed GC
[小红书]R1 #5348 CMS 和 G1 均使用三色标记法。跨代引用怎么办?g1 引入了 remember set
答:三色标记+漏标解法各异(CMS 增量更新、G1 SATB);G1 用 RSet 解决跨 Region 引用
[快手]R2 #2813 你了解哪些收集器?CMS 和 G1。详细谈谈 G1 的优点
答:G1 大堆友好+可预测停顿+无碎片;CMS 适合中小堆但 JDK 14 已移除
[携程]R1 #2920 如何判断一个对象是否要被回收?
答:可达性分析;CMS/G1 都基于 Roots 出发,不可达=垃圾
collector-selection
🎯 骨架
选型核心三要素:堆大小、延迟要求(P99)、JDK 版本
决策矩阵:
堆 < 4GB + 吞吐优先 → Parallel GC(JDK 8 默认 ParallelOld)
堆 4-32GB + 通用 → G1(JDK 9+ 默认,万金油)
堆 8GB+ + P99 严格 → ZGC(JDK 17+ LTS 推荐)
客户端/小堆/Demo → Serial GC(单线程)
JDK 版本约束:
JDK 8:Parallel(默认)/ CMS(已废弃)
JDK 11:G1 默认 + ZGC 实验
JDK 17:G1 默认 + ZGC 生产可用
JDK 21:分代 ZGC 生产就绪
历史遗留:JDK 8 + CMS 是大量在用的"老服务"组合,新建项目不要再选 CMS(JDK 14 已移除)
选 ZGC 的代价:读屏障 ~5-10% 吞吐下降、堆 < 32GB 时压缩指针支持滞后(JDK 15+ 才支持)
选 G1 的代价:RSet 占堆 5-10% 内存;不预测停顿不可能在所有场景达到 MaxGCPauseMillis
选错的征兆:批处理用 ZGC(吞吐打折)、大堆用 Parallel(停顿暴长)、延迟敏感用 CMS(碎片化退化 Full GC)
⚠️ 易混淆
"默认收集器"不等于"最优",要结合服务特征选
堆 32GB+ 不强制 ZGC,G1 仍可用(停顿可能拉长)
选型决策要看 P99 而不是平均 RT(用户感受是 P99)
Parallel 适合后台批处理(数据导出/对账),不适合 C 端在线服务
🎤 面经
[腾讯]R1 #2194 知道 G1 的回收机制吗?什么时候不用 G1?
答:小堆 < 4GB 用 Parallel 吞吐更高;超大堆 + 严格 P99 → 换 ZGC(读屏障代价换尾延迟稳定)
[快手]R1 #2746 为什么你的项目里用的是 CMS 收集器?
答:JDK 8 历史包袱+延迟敏感选 CMS;现在新项目升 JDK 17+G1 是常规选项
[京东]R2 #3062 JVM 垃圾回收器分类特点 实际使用场景
答:Serial 客户端、Parallel 后台批处理、CMS 老低延迟(已废)、G1 通用、ZGC 大堆延迟敏感
[美团]R3 #4956 常用的垃圾回收器?常用的 GC 调优策略?
答:选型 + 堆大小 + 关键参数;选型:< 4G Parallel、4-32G G1、>8G+严格延迟 ZGC
[快手]R2 #6378 你了解哪些收集器?详细谈谈 G1 的优点
答:G1 通用、可预测停顿、大堆友好、无碎片;JDK 9 起默认
[百度]R1 #2448 G1 回收器之后的回收器有了解吗
答:ZGC/Shenandoah;超大堆+严格延迟场景;JDK 21 分代 ZGC 生产就绪是新基线
[Shopee]R1 #800 ZGC 实践难点:TB 级堆停顿时间波动
答:分代 ZGC + ZAllocationSpikeTolerance + 30% Free 缓冲;监控 Allocation Stall
[其他][R1] 你怎么决定用哪个 GC?
答:先看 JDK 版本+堆大小+P99 要求;批处理 Parallel、在线 G1、超大堆/严格 P99 ZGC;测试压测验证
g1-tuning
🎯 骨架
核心目标参数:-XX:MaxGCPauseMillis=200(默认,软目标,G1 在预算内贪心选 Region)
触发标记的阈值:-XX:InitiatingHeapOccupancyPercent=45(IHOP,老年代占用 % 触发并发标记,JDK 9+ 自适应)
预留空间防 To-space Exhausted:-XX:G1ReservePercent=10(默认 10%,紧张时调到 15-20%)
Region 大小:-XX:G1HeapRegionSize(默认自动算,目标 ~2048 个 Region;大对象多时手动调大避免 Humongous)
混合回收容忍度:-XX:G1HeapWastePercent=5(老年代垃圾比例 < 5% 停止 Mixed GC,调小逼 G1 多清)
典型问题与调优:
To-space Exhausted → 加堆 / 增大 G1ReservePercent / 排查泄漏
Humongous 频繁 → 增大 RegionSize / 流式处理避免大对象
Mixed GC 跟不上 → 降低 IHOP(30-40%)提前触发标记 / 加堆
MaxGCPauseMillis 经常超 → 加堆(让 G1 有更多 Region 选择空间)/ 调大 RegionSize
G1 vs CMS 调优思路差异:CMS 重点防碎片化(CMSInitiatingOccupancyFraction),G1 重点防 Free Region 耗尽(G1ReservePercent + IHOP)
⚠️ 易混淆
MaxGCPauseMillis 是软目标,不保证 100% 满足(分配速率超回收速率仍会超)
IHOP 不是越小越好,过小导致 GC 频繁占 CPU;建议 30-45 之间
G1 不要设 -Xmn(会被忽略),用 -XX:G1NewSizePercent / G1MaxNewSizePercent 控制 Eden 比例
调一个参数压测对比一次,多个参数同时改无法定位有效改动
🎤 面经
[滴滴]R3 #2982 G1 如何实现可预测的停顿时间?
答:MaxGCPauseMillis 预算 + 追踪每个 Region 回收价值 + 贪心选高价值 Region + Mixed GC 多轮
[京东]R1 #3056 为什么用 G1?region 大小?
答:可预测停顿 + 大堆友好 + 无碎片;Region 1-32MB JVM 自动算,必要时调大避免 Humongous
[快手]R1 #2746 有尝试过 JVM 的调优吗
答:G1 基本盘 -XX:+UseG1GC -XX:MaxGCPauseMillis=200;问题导向调 IHOP/G1ReservePercent
[蚂蚁]R1 #6748 Remember Set 底层是怎么实现的?
答:每个 Region 一张 Hash 表/Card Table,写屏障在跨 Region 赋值时把发起 Region 卡号写入目标 RSet
[美团]R3 #4956 常用的 GC 调优策略?G1 调优?
答:MaxGCPauseMillis 200 基线;问题导向调 IHOP/Reserve/RegionSize;火焰图找大对象/泄漏
[京东]R1 #5471 g1 什么场景触发 full gc?
答:To-space Exhausted(Free 耗尽)/ Humongous 失败 / 元空间溢出;调优防前两类
[其他][R1] G1 频繁 Full GC 怎么调?
答:先看日志原因,To-space Exhausted 就加堆/调 ReservePercent;Humongous 多就调 RegionSize;Mixed 跟不上就降 IHOP
[其他][R1] G1 的 MaxGCPauseMillis 设多少合适?
答:默认 200;在线服务 100-200;金融/广告竞价可降到 50;越小堆越被压缩、GC 越频繁,要权衡
zgc-production
🎯 骨架
典型适用场景:在线服务大堆(8GB-TB)+ P99 严格的延迟敏感(金融交易、实时风控、广告竞价、推荐召回、网关)
关键启动参数:
-XX:+UseZGC(JDK 11+) / -XX:+UseZGenerational(JDK 21+ 分代 ZGC)
-Xms=-Xmx(启动时锁定堆,避免动态扩容停顿)
-XX:SoftMaxHeapSize(软上限,引导 ZGC 提前回收)
-XX:ZAllocationSpikeTolerance=2.0(默认 2.0,提高对突发流量容忍度)
监控核心指标:STW(Mark Start/End/Relocate Start,目标 <10ms)、Allocation Stall(堆满阻塞分配)、并发周期时长、堆使用率
TB 级堆调优要点:分代 ZGC(JDK 21+)减并发标记量、增大 ZCollectionInterval 防过度回收、保留 ~30% Free Region 应对突发
常见问题:
Allocation Stall:自适应预测失准,分配速率 > 回收速率 → 加堆 / 调 ZAllocationSpikeTolerance / 排查泄漏
吞吐下降:读屏障 ~5%,CPU 密集型场景明显,可换 G1
堆外/Metaspace 仍可能 OOM:ZGC 只管堆
生产前必做:压测 + GC 日志(-Xlog:gc*,gc+heap=debug)+ 火焰图对比 G1 基线
降级预案:JVM 参数支持热切换(不行)→ 实例蓝绿部署回退 G1;保留 G1 启动脚本
⚠️ 易混淆
ZGC 不是"无 STW",是"3 次 STW 各 <1ms"
ZGC 不是"用了就比 G1 快"——平均 RT 反而略高,换的是尾延迟稳定
JDK 15 之前 ZGC 不支持压缩指针,堆 < 32GB 时 G1 内存利用率更好
Allocation Stall 不算 Full GC,但应用线程被阻塞等 GC 腾空间
🎤 面经
[Shopee]R1 #800 ZGC 实践难点:如何解决 ZGC 在 TB 级堆内存下的停顿时间波动?
答:分代 ZGC 降标记量;调 ZAllocationSpikeTolerance;监控 Allocation Stall;保留 30% Free 应对突发
[美团]R3 #4956 常用的垃圾回收器?G1 和 ZGC 原理?常用的 GC 调优策略?
答:G1 Region+预测停顿;ZGC 染色指针+读屏障;调优三件套——堆大小、回收器、关键参数
[百度]R1 #2448 G1 回收器之后的回收器有了解吗
答:ZGC/Shenandoah;染色指针/Brooks 转发;STW<10ms 与堆无关;适合大堆低延迟
[腾讯]R1 #2194 知道 G1 的回收机制吗?为什么它被称为并发低延迟回收器?什么时候不用 G1?
答:G1 并发标记+局部 Mixed;超大堆+严格 P99 → 换 ZGC;小堆+追求吞吐 → Parallel
[其他][R1] 你们生产用的什么回收器?为什么?
答:答(参考):JDK 8 历史包袱用 CMS(已移除);JDK 11+ 升 G1;新建延迟敏感服务上 ZGC
[其他][R1] ZGC 怎么排查 Allocation Stall?
答:GC 日志看 Allocation Stall 频率+时长;jstack 看应用线程是否在 ZAlloc 阻塞;压测重现
[其他][R1] 为什么 ZGC 平均 RT 反而高?
答:读屏障每次引用读取都执行,持续消耗 CPU 导致单位时间处理请求数下降,但消除 STW 尖刺
[其他][R1] ZGC 适合什么场景,什么场景不适合?
答:在线服务+大堆+P99 严格 → 适合;批处理+追求吞吐+小堆 → 不适合(读屏障吃吞吐)
S4 类加载与字节码
classloading-process
🎯 骨架
七阶段口诀:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载(链接 = 验证+准备+解析)
加载:把 .class 字节流读进内存,生成 Class 对象(堆中),方法区放类元数据
验证:文件格式 → 元数据 → 字节码 → 符号引用,防止恶意字节码崩 JVM
准备:静态变量分配内存 + 赋零值(int=0/Object=null),static final 编译期常量直接赋真值
解析:符号引用 → 直接引用(内存地址),可发生在初始化前后(多态有动态解析)
初始化:执行 ``(静态变量赋真值 + static 块),JVM 加锁保证单线程,触发条件 = 6 种主动引用
<code>vs</code>:clinit 类级一次、JVM 加锁;init 实例级每次 new,不保证线程安全
⚠️ 易混淆
准备阶段只赋零值,初始化阶段才赋真值(避免依赖未就绪的类)
通过子类访问父类静态字段、定义数组、访问编译期常量 → 不触发初始化
Class.forName 默认触发初始化,ClassLoader.loadClass 不触发
静态内部类单例利用 clinit 加锁特性 → 懒加载 + 线程安全
🎤 面经
[快手]R2 #6325 Java 类加载的过程,分几步?类初始化的过程?
答:加载/验证/准备/解析/初始化;初始化执行 clinit,静态变量赋真值 + static 块
[蚂蚁]R1 #6720 类加载过程?双亲委派机制及使用原因?
答:5 阶段;委派父加载器,避免核心类被覆盖(沙箱),缓存命中
[腾讯]R2 #3125 java 如何从源代码转换成机器码执行的
答:javac 编译 .class → ClassLoader 加载 → 验证准备解析 → 初始化 → JIT 把热点字节码编机器码
[携程]R3 #2918 类的加载过程?类加载器有哪些?双亲委派模型?
答:5 阶段;Bootstrap/Ext/App + 自定义;委派父加载器优先,保证核心类一致性
[百度]R2 #2817 说一下 Java 类加载机制
答:加载/验证/准备/解析/初始化;委派、可见性、唯一性三特性
[蚂蚁]R1 #6894 说说类加载机制,可以自定义类加载器吗,为什么要自定义?
答:5 阶段;可以;热部署、加密 class、隔离(Tomcat、OSGi)、从网络/DB 加载
[网易]R3 #3190 java 虚拟机类加载机制
答:加载/链接/初始化;触发时机 6 种主动引用;clinit 一次 init 每次
[携程]R1 #2899 类的加载过程 能不能修改字节码
答:5 阶段;可以,ASM/Javassist/ByteBuddy 在加载阶段织入;Java Agent 也行
[字节]R2 #291 Java 类加载器的加载过程?验证阶段出现报错会怎么样?三层结构?
答:加载/链接/初始化;验证失败抛 VerifyError;Bootstrap/Ext/App 三层
parent-delegation
🎯 骨架
三层(JDK 8):Bootstrap(C++,加载 rt.jar)→ Extension(jre/lib/ext)→ Application(classpath)→ 自定义
委派流程:先在缓存找 → 找不到委派父加载器 → 父递归到 Bootstrap → 父加载不了再自己 findClass
三大作用:① 安全(核心类不被覆盖,自己写 java.lang.String 加载不上)② 唯一性(同一类只加载一次,相同 class 在不同加载器是不同 Class 对象)③ 缓存命中
JDK 9+ 模块化:Bootstrap → Platform → Application(不再叫 Ext,加载平台模块)
父加载器是组合关系(parent 字段),不是继承关系
类的"相同"判定:全限定名 + ClassLoader 双重标识,缺一不可
loadClass 走双亲委派、findClass 是自定义加载入口(重写它即可)
⚠️ 易混淆
"双亲"是翻译失误,实际只有一个父加载器(parent delegation)
Bootstrap 用 C++ 实现,getClassLoader() 返回 null
自定义加载器要破坏委派 → 重写 loadClass;只是加密/网络加载 → 重写 findClass 即可
同一个 class 文件被两个不同加载器加载 → 强转报 ClassCastException
🎤 面经
[蚂蚁]R1 #6720 类加载过程?双亲委派机制及使用原因?
答:先委派父加载器,找不到再自己加载;防核心类被覆盖、保证唯一性、缓存命中
[百度]R2 #7179 类加载过程?常用的类加载器?调用顺序?为啥双亲委派?
答:Boot/Ext/App+自定义;自下而上委派,自上而下加载;安全 + 唯一性
[快手]R1 #2740 类加载机制双亲委派;最终是哪个类来加载
答:从 App 一路委派到 Boot,能加载就加载;加载不了沿路返回,最后由能加载的子加载器加载
[美团]R1 #4891 java 双亲委派 为什么要用这种机制
答:核心类只能被 Bootstrap 加载,避免 java.lang.String 被恶意覆盖;多层级保证唯一性
[滴滴]R3 #2576 类加载器,双亲委派机制
答:3 层 + 自定义,先父后子;保证核心类一致性,类的唯一性靠 全限定名+ClassLoader 双标识
[字节]R2 #326 Java 为什么要设计双亲委派模型?什么时候需要自定义类加载器?
答:安全 + 唯一性;热部署、加密 class、应用隔离(Tomcat/OSGi)、从网络/DB 加载
[蚂蚁]R1 #6822 聊一聊类加载的过程?双亲委派机制及其使用原因?
答:5 阶段;委派父优先;安全防止核心类覆盖、保证 Class 对象唯一
[蚂蚁]R4 #4176 Tomcat 了解么,说一下类加载器结构吧
答:Bootstrap → Common → Catalina/Shared → 每个 WebApp 一个 WebAppClassLoader(先自己后父)
break-parent-delegation
🎯 骨架
本质:重写 ClassLoader.loadClass() 改顺序——先自己 findClass,找不到再委派父
三种典型场景:
Tomcat WebAppClassLoader:每个 WebApp 一个加载器,先 WEB-INF/classes 和 WEB-INF/lib,找不到才委派父——实现应用隔离(两个 app 同名类互不影响)
JDBC ServiceLoader / SPI:DriverManager 在 rt.jar,由 Bootstrap 加载,但驱动在 classpath,Bootstrap 看不到 → 用 Thread.currentThread().getContextClassLoader() 拿到 AppClassLoader 加载驱动
OSGi / 热部署:模块互相隔离,按需委派
Tomcat 顺序反转规则:JDK 核心类(java.*)仍委派父保证安全;应用类(WEB-INF/lib)才反转隔离
同名类的 Class 对象不相等:两个 WebApp 各自的 WebAppClassLoader 加载同一份 jar,得到两个 Class 对象
打破 ≠ 抛弃:核心类仍走委派,只对应用类反转,安全和隔离同时满足
⚠️ 易混淆
JDBC 不是"让 Bootstrap 加载驱动",是绕过 Bootstrap 改用 AppClassLoader
线程上下文加载器(TCCL)就是为了反向委派而设计的逃生通道
Tomcat 不是完全打破委派,对核心类还是委派(保证 javax.servlet.* 共享)
自定义 ClassLoader 重写 findClass 不是破坏委派,重写 loadClass 才是
🎤 面经
[蚂蚁]R4 #4176 Tomcat 了解么,说一下类加载器结构吧
答:Bootstrap → Common → Catalina/Shared → WebAppClassLoader;先自己后父,实现应用隔离
[携程]R1 #3735 什么是 tomcat 类加载机制?
答:每个 WebApp 独立 ClassLoader 反转双亲委派顺序,先 WEB-INF 后委派父,实现 app 间隔离
[美团]R3 #1217 jvm 类加载器,自定义类加载器,双亲委派机制,优缺点,tomcat 类加载机制
答:3 层;自定义重写 findClass;委派保证核心类安全;Tomcat 反转顺序实现 WebApp 隔离
[京东]R3 #2045 JVM 加载 class 文件原理?Tomcat 的 class 加载的优先顺序?
答:5 阶段;Tomcat 先 WEB-INF/classes → WEB-INF/lib → 委派 Common → Bootstrap
[字节]R2 #326 Java 为什么要设计双亲委派模型?什么时候需要自定义类加载器?
答:安全 + 唯一性;热部署、加密 class、应用隔离(Tomcat/OSGi)、从网络/DB 加载
[蚂蚁]R1 #6894 说说类加载机制,可以自定义类加载器吗,为什么要自定义?
答:可以,重写 findClass;典型场景热部署、加密、Tomcat 隔离、从 jar/网络加载
[蚂蚁]R4 #3444 了解 class loader 的内存泄露吗?出现了内存泄露该怎么办?
答:静态变量持有 instance → 持有 Class → 持有 ClassLoader → 类卸载三条件不满足 → Metaspace 涨;改弱引用或显式 remove
S5 调优与排障实战
oom-troubleshoot
🎯 骨架
OOM 类型识别(java.lang.OutOfMemoryError: ...):
Java heap space:堆 OOM,最常见,集合/缓存/大对象
Metaspace:类加载泄漏(动态代理/反射/热部署)
Direct buffer memory:堆外内存,NIO/Netty
unable to create new native thread:线程数 > 系统/容器限制
GC overhead limit exceeded:>98% 时间 GC 但回收 < 2%
PermGen space:JDK 7-,已被 Metaspace 取代
预设兜底(必加):-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/oom.hprof,OOM 自动 dump
三步排查法:
保留现场:dump 已自动产生 / 手动 jmap -dump:format=b,file=x.hprof
MAT 分析:① Leak Suspects Report 自动找嫌疑对象 ② Dominator Tree 看占内存最大对象树 ③ 顺着 GC Root 引用链找业务代码
定位根因:常见 4 类(无分页全量查询 / 静态集合无限增长 / 缓存无过期策略 / ThreadLocal 未 remove)
常见 OOM 根因:
静态集合无限增长(static List/Map)
缓存无过期策略(Guava Cache 没设 expireAfterWrite)
大批量加载(Excel 导出/Mybatis 全量查 20W 行 → JDBC + ORM 内存放大 25 倍)
ThreadLocal 没 remove(线程池复用泄漏)
类加载器泄漏(动态代理/Aviator → Metaspace OOM)
关键命令链:top -Hp → jstat -gcutil 1000 → jmap -dump → MAT
排查到代码后:修复(加分页/加过期/finally remove/改弱引用)+ 压测验证堆稳定 + 监控大盘加报警
生产防御:限制 dump 大小、生产慎用 jmap 在线(触发 Full GC + 写盘秒级卡顿)、优先用 async-profiler 火焰图
⚠️ 易混淆
内存溢出 ≠ 内存泄漏:溢出 = 真不够;泄漏 = 不该活的还活着(强引用未释放)
jmap dump 会触发 Full GC + 写磁盘秒级卡顿,谨慎线上用
ThreadLocal 泄漏前提:线程池(线程复用)+ 不 remove,缺一不可
GC overhead limit exceeded 比 heap space 更早,是早期预警
🎤 面经
[腾讯]R1 #3114 介绍你项目中排查 OOM 的经历
答:告警→保留现场→看 GC 日志/dump→jprofile/MAT 看对象内存结构→定位代码(无分页全量查询)→ 修复压测验证
[京东]R1 #5478 有没有碰到 OOM 的问题,怎么定位解决的
答:HeapDumpOnOutOfMemoryError 兜底→MAT Leak Suspects→Dominator Tree→顺 GC Root 链→修代码
[蚂蚁]R4 #4155 有没有处理过线上疑难问题?OOM 如何排查与解决?
答:4 类 OOM 定位类型→dump+MAT→定位静态集合/缓存/ThreadLocal/大查询;修复+加监控
[蚂蚁]R1 #4101 有没有解决过 oom 问题,介绍一下
答:典型场景:批量导出无分页/缓存无过期/动态代理 Metaspace;MAT 找根+流式处理修复
[滴滴]R2 #2503 解决过什么感觉比较复杂的问题,线上 OOM 排查过程
答:HeapDump+MAT→Dominator 看到 List 异常大→trace 到接口→无分页全量查询;改分页+流式
[腾讯]R1 #2194 什么情况下会出现 oom,举个例子
答:堆 OOM(静态集合/缓存/大查询)/Metaspace(类加载泄漏)/Native Thread(线程数过多)
[拼多多]R1 #872 线上排查 OOM(内存溢出)问题怎么做?查过 Linux 下内存高的进程吗?
答:top→jstat→jmap dump→MAT;Linux ps aux --sort=-%mem 看进程;free -h 看 RSS
[阿里]R5 #209 项目遇到最大的问题(OOM)。jvm oom 排查 (Java heap space)
答:MAT 工具分析内存泄漏→定位到学生作业报告接口→无分页全量查询→改分页+缓存
fullgc-troubleshoot
🎯 骨架
四步排查法:
Step 1 关联分析:近期变更 + 上下游告警 + 时间相关性(业务高峰/批量任务)
Step 2 异常指标:高耗时接口、慢 SQL、批量 JOB、消息陡增、缓存击穿
Step 3 GC 日志分类找根因:
CMS → 看 Concurrent Mode Failure / Promotion Failed(碎片化)
G1 → 看 to-space exhausted / Humongous Allocation
通用 → 看 Metaspace OOM / System.gc()
Step 4 分类排查:大对象用火焰图找分配热点;Metaspace 查类加载器异常(动态代理/反射/热部署)
核心命令链:
jstat -gcutil 1000:实时看老年代/元空间占用变化
jmap -histo:live :对比两次快照看哪些类实例数异常增长
jmap -dump:format=b,file=oom.hprof :抓堆快照,MAT 分析
jstack :看线程栈是否在大对象分配热点
CMS 频繁 Full GC 三大根因:
碎片化(CMF/Promotion Failed)→ 升 G1 / 调 CMSInitiatingOccupancyFraction 到 70-75
老年代基线高(缓存/连接池/泄漏)→ 加堆 / 排查泄漏
大对象进 Old → 流式处理 / 拆分
G1 频繁 Full GC 三大根因:
To-space Exhausted → 加堆 / 增大 G1ReservePercent / 排查泄漏
Humongous 挤占 → 增大 G1HeapRegionSize / 业务避免大数组
Mixed GC 跟不上 → 降低 IHOP 提前标记
元空间溢出特征:堆使用率不高但 Full GC 频繁 → jstat -gcmetacapacity 看 Metaspace 是否持续增长
关键 JVM 参数:-Xlog:gc* / -XX:+HeapDumpOnOutOfMemoryError / -XX:HeapDumpPath=/tmp/oom.hprof
预防大于治疗:合理设堆+选 GC+设 MaxMetaspaceSize 兜底+监控大盘报警
⚠️ 易混淆
频繁 Full GC ≠ 一定堆使用率高,可能是元空间溢出或显式 System.gc()
jmap -dump 会触发 Full GC + 写磁盘,对生产有秒级卡顿,谨慎在线上用
火焰图(async-profiler)开销小,可线上常驻,优先于 jmap dump
Mixed GC 频率上升不等于 Full GC,但持续上升是 Full GC 前兆
🎤 面经
[阿里]R3 #66 发生 fullGC 怎么排查?
答:先看 GC 日志找原因(CMF/碎片/Humongous/Metaspace)→ jmap 对比快照找泄漏类 → 火焰图定位分配热点 → 调参或加堆
[阿里]R1 #192 怎么去分析 Full GC
答:四步法(关联→指标→GC 日志→分类排查);CMS 看 CMF;G1 看 to-space exhausted;元空间看 jstat
[滴滴]R1 #3027 fullgc 怎么解决
答:先定原因(碎片/泄漏/大对象/元空间);治标调参(IHOP/Reserve),治本改代码(流式/缓存过期)
[网易]R1 #1743 频繁 FGC 怎么排查?
答:jstat 看 metaspace+gc 频次;jmap dump 用 MAT 找 Leak Suspects;GC 日志看触发原因
[快手]R2 #6378 什么时候进行 Full GC 呢?
答:通用 5 类 + CMS CMF/Promotion Failed + G1 To-space Exhausted;排查从 GC 日志入手
[腾讯]R1 #3114 介绍你项目中排查 OOM 的经历(关联频繁 Full GC)
答:告警→看日志→jmap dump→jprofile/MAT 看 Dominator→定位代码(无分页全量查询/ThreadLocal 泄漏)
[快手]R1 #2746 有尝试过 JVM 的调优吗
答:CMS Initiating 70%、+CMSScavengeBeforeRemark、Xms=Xmx 锁定;切 G1+MaxGCPauseMillis 200
[其他]R1 #4429 线上问题排查案例讲一个。Fullgc,如何用 arthas 定位到那个大的 list 的?
答:arthas dashboard 看 GC 趋势;watch / trace 找哪个方法返回大集合;profiler start 抓火焰图
jvm-tools
🎯 骨架
JDK 自带工具速查:
jps:列 Java 进程 PID
jstat -gcutil 1000:实时看 GC(YGC/FGC 次数+耗时、Eden/Old/Metaspace 占比)
jstack :抓 Thread Dump,看线程状态+栈帧(CPU 飙高/死锁/线程阻塞)
jmap -histo:live :对象直方图,对比快照找异常增长类
jmap -dump:format=b,file=x.hprof :堆快照(会触发 Full GC,谨慎)
jcmd VM.flags / GC.heap_info / VM.classloader_stats:替代很多 jstat/jmap 用法,更现代
jhsdb / jinfo:查看 JVM 配置/启动参数
CPU 飙高 SOP:top → top -Hp → 找最高线程 tid → printf '%x\n' → jstack | grep -A 30 定位栈
OOM SOP:-XX:+HeapDumpOnOutOfMemoryError 自动 dump → MAT 看 Leak Suspects + Dominator Tree → 顺 GC Root 链找代码
MAT 三板斧:Leak Suspects(自动嫌疑报告)/ Dominator Tree(最大对象树)/ Path to GC Roots(引用链回溯)
Arthas(线上诊断神器):
dashboard 看 GC + 线程 + 内存全景
thread 看线程栈、thread -b 找死锁
watch / trace 看方法入参/返回/耗时
jad 反编译已加载类
profiler start --duration 30 抓火焰图(async-profiler 内置)
redefine 热更新 class(线上 hotfix)
async-profiler:火焰图利器,开销 < 5%,可线上常驻;优于 jmap dump
VisualVM / JProfiler:本地分析 hprof + 实时监控;Idea 集成方便
⚠️ 易混淆
jmap dump 触发 Full GC + 写磁盘,秒级卡顿,生产慎用;优先 async-profiler
Thread Dump = jstack,kill -3 也能触发但输出到 stdout
-Hp 是 top 看线程 ID,不是直接看进程
Arthas 在容器里要附加进程,注意权限(同 UID 才行)
🎤 面经
[蚂蚁]R1 #6666 jvm 底层原理和排查命令
答:jps/jstat/jstack/jmap/jcmd;CPU 飙高用 top+jstack;OOM 用 jmap dump+MAT
[快手]R1 #2747 线上遇到问题,有用过阿里开源的 Arthas 吗
答:用过;dashboard/thread/watch/trace/profiler 抓火焰图/redefine 热更
[阿里]R1 #132 平时遇到过 gc 吗,怎么定位并解决问题的,说一下怎么用 arthas 的
答:dashboard 看 GC 趋势;profiler 抓火焰图找分配热点;watch 看方法返回;jad 反编译验证逻辑
[其他]R1 #4369 性能优化做过吗?arthas,优化流程,CMS 优化
答:arthas profiler 抓火焰图找热点;调 CMSInitiatingOccupancyFraction+CMSScavengeBeforeRemark
[其他]R1 #4429 线上问题排查案例讲一个。Fullgc,如何用 arthas 定位到那个大的 list 的?
答:dashboard 看 GC;profiler 火焰图找大对象分配栈;trace 方法返回值 size;jad 反编译确认
[快手]R1 #2614 线上遇到问题,有用过阿里开源的 Arthas 吗
答:用过;优于 jstack+jmap 因为不停应用;watch/trace 看实时方法行为
[拼多多]R1 #872 线上排查 OOM 问题怎么做?
答:top 看进程→jstat 看 GC→jmap dump→MAT 分析→定位代码;线上优先 async-profiler 火焰图
[腾讯]R1 #3114 介绍你项目中排查 OOM 的经历
答:告警→看日志→jmap dump→jprofile/MAT 看 Dominator→定位代码(无分页全量查询)
jvm-tuning
🎯 骨架
调优 ≠ 改参数,先解决代码问题(缓存无过期、ThreadLocal 未 remove、大查询无分页)
三个核心维度:堆大小、GC 选型、关键参数
堆大小:
-Xms = -Xmx(启动时锁定,避免动态扩容触发 Full GC + STW)
容器化场景必加 -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0(JDK 8u191+)
元空间设上限:-XX:MaxMetaspaceSize=256m(兜底防类加载泄漏)
GC 选型(详见 collector-selection 卡):
堆 < 4GB + 吞吐 → Parallel
4-32GB + 通用 → G1
8GB+ + P99 严格 → ZGC
GC 日志必加(出问题没日志等于盲人):
JDK 8:-XX:+PrintGCDetails -Xloggc:/tmp/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M
JDK 9+:-Xlog:gc*:file=/tmp/gc.log:time,uptime:filecount=10,filesize=10M
OOM/异常兜底:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/oom.hprof
-XX:+ExitOnOutOfMemoryError(K8s 场景,OOM 直接退出让容器重启)
调优 SOP:压测拿基线 → 看 GC 日志/火焰图找瓶颈 → 改一处参数 → 再压测对比 → 一次一变量
⚠️ 易混淆
"调大堆"不一定好,Young GC 时间随 Eden 增大而增加
-Xmn(设新生代大小)和 G1 不兼容,G1 会忽略
容器场景不加 UseContainerSupport,JVM 看到的是宿主机内存,会 OOM Killer
System.gc() 不会立刻 GC,只是建议;要禁用用 -XX:+DisableExplicitGC
🎤 面经
[阿里]R2 #29 jvm 调优参数
答:堆大小 Xms=Xmx;GC 选型;GC 日志 Xlog:gc*;OOM 兜底 HeapDumpOnOutOfMemoryError;MaxMetaspaceSize 兜底
[快手]R1 #2746 有尝试过 JVM 的调优吗
答:CMS Initiating 70-75;+CMSScavengeBeforeRemark;Xms=Xmx 锁定;切 G1 + MaxGCPauseMillis 200
[美团]R3 #4956 常用的 GC 调优策略?
答:先解决代码问题;选合适 GC;调堆+关键参数;看 GC 日志;火焰图找热点;一次一变量
[其他]R1 #4369 性能优化做过吗?arthas,CMS 优化
答:CMSInitiatingOccupancyFraction 70-75;+CMSScavengeBeforeRemark 缩重标停顿;arthas 火焰图找热点
[腾讯]R1 #2194 有哪些垃圾回收器?G1 为什么叫并发低延迟?什么时候不用 G1?
答:选型决策树(堆大小+延迟+JDK 版本);超大堆+严格 P99 用 ZGC;批处理用 Parallel
[其他][R1] JVM 调优的指标是什么?
答:吞吐(业务 QPS)+ 延迟(P99 RT)+ GC 时间占比;目标看场景,在线服务追 P99,批处理追吞吐
[其他][R1] 容器化场景 JVM 调优注意什么?
答:UseContainerSupport(看到容器内存)+ MaxRAMPercentage 75%;Xms=Xmx;ExitOnOutOfMemoryError
[其他][R1] 你们生产 JVM 启动参数大概什么样?
答:答(参考):-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xlog:gc* -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=256m
memory-leak-detection
🎯 骨架
泄漏 vs 溢出:
泄漏(Leak):不该活的对象还活着(强引用未释放,GC 无法回收)
溢出(OOM):内存真的不够用(数据量超过堆容量)
泄漏积累到一定程度 → 溢出
四大泄漏典型场景:
静态集合无限增长:static List/Map,进多出少,永远不释放
ThreadLocal 未 remove:线程池场景下 Value 强引用挂在 Thread.threadLocals
缓存无过期策略:Guava Cache 没设 expireAfterWrite/maximumSize
类加载器泄漏:静态变量持有 instance → Class → ClassLoader 不能回收 → Metaspace 涨
ThreadLocal 泄漏根因链:Key 弱引用(GC 后变 null)+ Value 强引用 + 线程池线程不死 → Value 永远泄漏 + 数据错乱风险
类加载器泄漏链:GlobalCache.cache → instance → Class → ClassLoader → 类卸载三条件不满足 → 每次 reload 类描述堆积 → Metaspace OOM
排查工具:
MAT:Leak Suspects Report 自动找嫌疑、Dominator Tree 看大对象、查 GC Root 引用链
VisualVM / JProfiler:看堆变化趋势 + 实例数变化
jmap -histo:live:对比两次快照找异常增长的类
修复手段:
弱引用替代强引用(WeakHashMap)
显式 remove/clear(try-finally)
缓存设 expireAfterWrite + maximumSize
减少静态集合持有;持有时要有清理机制
防御:监控堆使用率长期趋势(线性增长是泄漏信号),加 -XX:+HeapDumpOnOutOfMemoryError 留取证据
⚠️ 易混淆
泄漏的对象是"可达"的(GC 找得到引用链),所以 GC 不动它
ThreadLocal 单独使用不泄漏(线程销毁就回收),泄漏前提是线程池
finalize 不能依赖(执行不确定 + 性能差),用 try-finally + close 或 PhantomReference
WeakHashMap 的 Key 是弱引用,但 Value 是强引用(同 ThreadLocal 套路)
🎤 面经
[滴滴]R2 #2504 拷打 JVM 基础概念,为啥 ThreadLocal 内存泄漏
答:Key 弱 GC 后变 null,Value 强引用挂在 Thread.threadLocals;线程池线程不销毁 → 必须 remove
[字节]R2 #325 你说的内存泄漏具体是怎么产生的?线程池的线程是不是必须手动 remove 才可以回收 value?
答:弱 Key 被 GC 变 null Entry,Value 仍被强引用;线程池线程不死必须 try-finally remove
[蚂蚁]R4 #3444 了解 class loader 的内存泄露吗?出现了内存泄露该怎么办?
答:静态变量持有 → Class → ClassLoader → 类卸载三条件不满足 → Metaspace 涨;改弱引用或 remove
[网易]R1 #3181 Java 会不会内存泄露?怎样会泄露?
答:会;典型 4 类(静态集合/ThreadLocal/缓存/类加载器);GC 找得到引用就不回收
[美团]R1 #1143 一般哪些原因会导致内存泄漏?
答:4 类典型;本质是强引用未释放(静态集合/事件监听未注销/ThreadLocal/Connection 未关)
[阿里]R1 #77 ThreadLocal 的实现原理靠什么?知道可能导致内存泄漏的原因后,具体怎么防范?
答:每个 Thread 一个 ThreadLocalMap;try-finally remove;MDC/上下文传递场景配合 InheritableThreadLocal
[得物]R1 #2687 JVM 是如何避免内存泄漏的?
答:可达性分析自动回收;提供软/弱/虚引用控制生命周期;finalize 兜底(不推荐)
[小米]R1 #2075 为什么存在内存泄漏?哪些对象可以作为 GC ROOT?如何排查?
答:强引用未释放;4 类 Root;jmap dump+MAT 看 Dominator+顺 GC Root 链
CONCURRENT
S1 线程的诞生与基础同步
synchronized-principle
🎯 骨架
★ 底层实现:字节码 monitorenter/monitorexit,运行时修改对象头 Mark Word 锁标志位
★ 锁升级(JDK 15+ 无偏向锁):无锁 → 轻量级锁(CAS)→ 重量级锁(Monitor)
★ 轻量级锁过程:线程在栈帧开辟 Lock Record → 备份对象 Mark Word → CAS 把对象 Mark Word 替换为 Lock Record 地址 → 解锁时 CAS 换回
重量级锁:CAS 自旋失败(自适应,JVM 根据历史成功率动态调整)→ 膨胀为 ObjectMonitor → 竞争失败线程挂起(OS park)
JDK 15 关闭偏向锁原因:现代应用多线程常态,偏向锁撤销需 STW,收益不抵成本。关闭后直接走轻量级锁 CAS,纳秒级开销差距极小
⚠️ 易混淆
锁升级不可降级
synchronized 轻量级锁场景下性能和 ReentrantLock 差不多,还有 JVM 级优化(锁消除/锁粗化)
ReentrantLock 优势在功能(公平锁/tryLock/多 Condition),不在性能
选型原则:能用 synchronized 就用,需要 tryLock/公平锁/多条件变量才上 ReentrantLock
🎤 面经
[小红书][R1] synchronized 底层流程?性能一定差吗?→ 改对象头 Mark Word,锁升级后无竞争接近零开销
[携程][R1] 锁升级过程?→ 无锁→轻量(CAS 栈 Lock Record)→重量(Monitor 阻塞)
[蚂蚁][R1] 偏向锁/轻量级/重量级分别解决什么?→ JDK15 已关偏向;轻量=交替/短暂竞争;重量=激烈竞争
lock-upgrade-detail
⚠️ 易混淆
偏向锁撤销需要 STW(安全点),不是"随时可以撤销"
轻量级锁靠 CAS 自旋不阻塞;重量级锁靠 OS 互斥量线程挂起进内核态
EntryList = 没拿到锁排队(BLOCKED);WaitSet = 拿到锁后主动 wait(WAITING),两者原因不同
G1 的转发地址写在 Mark Word 里(STW 内完成);ZGC 的转发表在 native memory(并发维护)
🎤 面经
[携程][R1] synchronized 锁升级过程?→ 无锁→偏向锁(CAS写线程ID)→轻量级锁(CAS换Lock Record)→重量级锁(OS互斥量)
[蚂蚁][R1] 偏向/轻量/重量级锁分别解决什么问题?→ 偏向=单线程无竞争;轻量=交替执行;重量=真实竞争
[网易][R1] 能否让 synchronized 一创建就是重量级锁,有什么坏处?→ 能,但每次加锁都走内核态切换,无竞争场景性能损耗极大;偏向/轻量级锁在用户态解决代价接近零
[小红书][R1] synchronized 底层流程?性能一定差吗?→ JDK6 后引入偏向/轻量级锁优化,无竞争时接近无锁性能
[美团][R1] 偏向锁升级过程?→ 首次加锁 CAS 写线程ID;再次进入只比较ID;其他线程竞争触发 STW 撤销→升轻量级锁
[Shopee][R1] 如何用 JOL 工具验证锁升级?→ JOL 打印对象头 Mark Word,观察 lock 标志位从 01→00→10 变化
sync-vs-lock
🎯 骨架
★ synchronized:JVM 内置,自动释放,锁升级(偏向→轻量→重量),简单场景首选
★ ReentrantLock:手动 unlock(finally 必须),支持公平锁/可中断/超时获取/多 Condition
★ 锁升级路径:无锁 → 偏向锁(单线程,只比较 threadId)→ 轻量级锁(CAS 自旋)→ 重量级锁(OS 互斥量,线程阻塞)
性能:JDK6+ 低竞争场景相当,高竞争 ReentrantLock 略优(AQS 更灵活)
AQS 核心:state(锁状态)+ CLH 双向链表(等待队列),CAS 抢锁失败则入队 park
公平锁 vs 非公平锁:公平锁多 hasQueuedPredecessors() 检查,吞吐量低 5-10 倍
⚠️ 易混淆
JDK15 默认禁用偏向锁(JEP 374):现代多线程场景偏向锁撤销 STW 代价 > 收益
轻量级锁升重量级锁条件:自旋超阈值 OR 已有线程在等待(竞争激烈)
ReentrantLock 必须在 finally 里 unlock,否则异常时锁不释放
🎤 面经
[携程][R1] synchronized 锁升级过程?→ 无锁→偏向锁(threadId)→轻量级锁(CAS自旋)→重量级锁(OS互斥量)
[快手][R1] synchronized 和 ReentrantLock 底层实现区别?→ JVM 内置 vs AQS;自动释放 vs 手动;无高级功能 vs 公平/可中断/超时
[腾讯][R1] 什么是 AQS?→ 抽象队列同步器,state + CLH 双向链表,CAS 抢锁失败入队 park 阻塞
[阿里][R1] AQS 核心数据结构?ReentrantLock 怎么利用 AQS 实现可重入?→ state 记录重入次数,同一线程 state++ 直接获取
[小红书][R1] synchronized 性能一定比较差吗?→ JDK6+ 锁升级优化后低竞争场景不差,偏向锁/轻量级锁都是用户态操作
wait-notify
🎯 骨架
wait():释放锁,线程进入 WAITING 状态,等待被唤醒(必须在 synchronized 块内调用)
notify():唤醒一个等待该对象锁的线程(随机),被唤醒线程重新竞争锁
notifyAll():唤醒所有等待该对象锁的线程,全部重新竞争锁
wait vs sleep:wait 释放锁,sleep 不释放锁;wait 需要在 synchronized 内,sleep 不需要;wait 被 notify 唤醒,sleep 超时自动醒
底层:wait/notify 依赖 Monitor(对象头 Mark Word 指向的 Monitor 对象),WaitSet 存放等待线程
⚠️ 易混淆
wait() 必须在 synchronized 块内,否则抛 IllegalMonitorStateException
notify() 唤醒后线程不立刻执行,需要重新竞争到锁才能继续
wait(0) = wait(),无限等待;wait(1000) 等待最多 1 秒
🎤 面经
[阿里 R5] wait 和 notify 的使用方式及原理?→ 基于 Monitor,wait 进 WaitSet 释放锁,notify 从 WaitSet 移到 EntryList 重新竞争
[小米 R1] wait、sleep、notify、notifyAll 的区别?→ wait 释放锁/需 synchronized,sleep 不释放锁/不需 synchronized
[快手 R1] 线程间通信方式?wait/notify/sleep 有什么区别?→ 通信方式:wait/notify、volatile、CountDownLatch、BlockingQueue 等
[蚂蚁 R1] wait 和 notify 的使用和原理?→ 同阿里 R5
[腾讯 R1] Thread.sleep() 和 Object.wait() 的区别?→ sleep 不释放锁,wait 释放锁;sleep 是 Thread 方法,wait 是 Object 方法
thread-lifecycle
🎯 骨架
NEW:创建未启动
RUNNABLE:运行中(含就绪态,Java 不区分 ready/running)
BLOCKED:等待 synchronized 锁
WAITING:无限等待(Object.wait()/Thread.join()/LockSupport.park())
TIMED_WAITING:有限等待(sleep(n)/wait(n)/join(n))
TERMINATED:执行完毕
⚠️ 易混淆
BLOCKED 只针对 synchronized,ReentrantLock 等待时是 WAITING
sleep 不释放锁,wait 释放锁
🎤 面经
[快手 1面] 线程的生命周期有哪些状态?→ NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED 六种
[拼多多 1面] 线程池中线程的生命周期是怎样的?线程是怎么复用的?→ 核心线程 WAITING 在 BlockingQueue.take(),有任务来被唤醒执行,执行完继续 take
[网易 1面] 线程和线程池的生命周期分别是怎样的?→ 线程六态;线程池:RUNNING→SHUTDOWN→STOP→TIDYING→TERMINATED
[携程 2面] Java 的线程有哪些状态?→ 六种,BLOCKED 只针对 synchronized,ReentrantLock 等待是 WAITING
[小米 1面] 线程的状态以及变化的时机?→ sleep/wait/join 进 TIMED_WAITING/WAITING,抢 synchronized 锁进 BLOCKED,执行完进 TERMINATED
[快手 2面] JVM 定义了几种线程状态?→ 六种枚举:NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED
[other 1面] WAITING 和 TIMED_WAITING 区别?什么时候进入 TIMED_WAITING?→ WAITING 无限等待需被唤醒,TIMED_WAITING 超时自动唤醒;sleep(n)/wait(n)/join(n) 进入 TIMED_WAITING
[网易 1面] 线程和线程池的生命周期分别是怎样的?→ 线程六态;线程池:RUNNING→SHUTDOWN→STOP→TIDYING→TERMINATED
[携程 1面] Java 多线程的实现方式有哪些?线程的生命周期是怎样的?→ 实现:继承 Thread/实现 Runnable/Callable+FutureTask;生命周期六态
[小米 1面] 线程的生命周期有哪些状态?→ 六种,重点区分 BLOCKED vs WAITING
visibility-problem
🎯 骨架
根因:CPU 多级缓存,各核心缓存数据不同步,写操作可能只在缓存中未刷主内存
volatile 解决可见性:写时插入 StoreLoad 屏障强制刷主内存,触发 MESI 协议让其他 CPU 缓存行 Invalid
volatile 解决有序性:四种内存屏障禁止指令重排序(屏障两侧指令不能越过屏障)
volatile 不保证原子性:i++ 是读-改-写三步,寄存器私有不受 MESI 管辖
synchronized 也保证可见性:退出 synchronized 块时强制刷新主内存
⚠️ 易混淆
volatile 保证可见性 + 禁止重排序,但不保证原子性
偶发性多线程问题根因是线程调度时序随机,不是缓存写入速度
x86 是 TSO 强模型,只有 StoreLoad 需要真正的 CPU 指令(mfence),其他三种是空操作
🎤 面经
[网易 1面] volatile 底层原理?→ 内存屏障(StoreLoad/LoadLoad)+ MESI 协议,写刷主内存,读失效缓存
[阿里 1面] volatile 的可见性、有序性、不保证原子性?→ 可见性靠 StoreLoad 屏障+MESI,有序性靠四种屏障禁止重排序,i++ 不原子因为寄存器私有不受 MESI 管辖
[阿里 2面] volatile 使用场景?→ 状态标志位(boolean stop)、DCL 单例(防止半初始化对象)
[字节 1面] 什么时候用 volatile?→ 一写多读的状态标志;DCL 双重检查锁中防止指令重排序
[拼多多 1面] 线程间数据不一致的根本原因?→ CPU 多级缓存,各核心缓存数据不同步;volatile/synchronized 强制刷新主内存解决
[通用] synchronized 和 volatile 都能保证可见性,区别?→ synchronized 还保证原子性(互斥),volatile 只保证可见性和有序性,不互斥
deadlock-detection
🎯 骨架
★ 4 个必要条件(缺一不死锁):互斥 / 持有并等待 / 不可抢占 / 循环等待
★ jstack 检测:jstack 输出末尾找 "Found one Java-level deadlock",会列出参与死锁的线程和锁地址
★ arthas thread -b:直接定位"导致其他线程阻塞的线程",比 jstack 更快定位元凶
★ 预防:破坏其中一个条件——最常用是固定加锁顺序(按对象 hashCode 排序加锁)破坏循环等待
★ 避免:tryLock(timeout) 拿不到就放弃;死锁发生概率高的业务直接换 Redis 分布式锁加超时
☆ 银行家算法:理论方案,工程几乎不用(开销大)
☆ 分布式死锁:分布式锁必设过期时间,避免节点崩溃后锁永远不释放
☆ 活锁 vs 饥饿:活锁是双方互相退让一直不前进;饥饿是低优先级线程永远拿不到资源
⚠️ 易混淆
死锁的"循环等待"必须是闭环,A 等 B、B 等 A 是最简形式
jstack 不是所有死锁都能检测,只能检测 Java 锁(synchronized + JUC Lock);Lock 死锁有时检测不到
tryLock 不是万能药——拿不到锁后必须有降级逻辑,不能直接抛异常
间隙锁死锁(MySQL)和 Java 死锁不同,需要看 InnoDB 状态
🎤 面经
[腾讯]R1 #3129 死锁是怎么产生的?
答:4 个必要条件同时成立:互斥+持有等待+不可抢占+循环等待
[滴滴]R1 #2978 死锁条件,解决方式?
答:4 条件;解决:固定加锁顺序破坏循环等待 / tryLock 超时 / Redis 锁加过期
[网易]R3 #3206 死锁怎么预防。怎么检测死锁?
答:预防:破坏 4 条件之一;检测:jstack 找 "Found one Java-level deadlock" 或 arthas thread -b
[蚂蚁]R4 #4149 死锁问题的分析与具体解决办法(非八股,具体排查思路)?
答:现场保留 jstack;找循环等待环;按业务拆分锁粒度或加超时;分布式锁加过期时间
[蚂蚁]R4 #4180 什么时候多线程会发生死锁,写一个例子?
答:两线程各持一锁请求另一锁形成环;写法 synchronized(A) { synchronized(B) {} } 反向同样
[网易]R1 #3226 手写死锁(Synchronized 和 ReentrantLock)?
答:两线程交叉持锁;ReentrantLock 用 tryLock(timeout) 可主动避免
[蚂蚁]R1 #6903 加锁有几种方式,死锁的条件,设计一个场景会不会死锁怎么避免?
答:synchronized/Lock/分布式锁;4 条件;按 ID 排序加锁避免环
[快手]R1 #2677 对线程安全有什么理解;内存泄露;死锁?
答:死锁 4 条件 + jstack 检测 + 预防(顺序加锁/tryLock/Redis 过期)
[美团]R4 #3981 高并发场景下如何防止死锁,保证数据的一致性?
答:缩小锁粒度 + 固定加锁顺序 + tryLock 兜底 + 分布式锁加过期;最终用对账机制
[蚂蚁]R4 #4150 间隙锁死锁原因与排查思路?
答:MySQL 死锁:show engine innodb status;间隙锁通过 RC 隔离或 SQL 优化避免
S2 无锁革命与内存模型
cas-principle
🎯 骨架
★ CAS(Compare-And-Swap)= 三元组 V/E/N:内存当前值 V == 期望值 E 时,写入新值 N,否则失败
★ 调用链:Java Unsafe.compareAndSwapInt → JVM native → CPU lock cmpxchg 指令(一条原子指令完成比较+交换)
★ 多核原子性靠两层:①CPU 单条指令不可中断(指令边界检查中断) ②lock 前缀触发缓存锁 + MESI 让其他核失效
★ 失败处理:自旋重试(do-while),不阻塞线程,无上下文切换
☆ vs synchronized:CAS 不阻塞、低竞争快、高竞争自旋耗 CPU;synchronized 阻塞、有上下文切换
☆ 三大问题:ABA、自旋空转、只能保证单变量原子(多变量需 AtomicReference 包装)
☆ 缓存锁粒度是缓存行(64B),跨缓存行才退化为总线锁;伪共享会让不同变量互相影响
⚠️ 易混淆
CAS 原子性是 CPU 硬件保证的,不是软件实现
lock 前缀强制同步等待所有核 ACK,不是异步的
高竞争下 CAS 性能反而下降(自旋 + 频繁 invalidate),此时锁更划算
AtomicInteger 内部就是 do { v = volatile读 } while (CAS 失败),自旋 CAS
🎤 面经
[阿里]R2 #52 介绍下 CAS(追问 ABA 如何解决)
答:比较交换三元组,CPU cmpxchg 一条原子指令完成;ABA 用 AtomicStampedReference 加版本号解决
[网易]R1 #1484 CAS 原理是什么?ABA 问题怎么解决?
答:Unsafe→JVM→CPU lock cmpxchg;ABA 加版本号或时间戳
[Shopee]R1 #5369 cas 实现流程,为什么觉得 cas 比 synchronized 性能好?
答:不阻塞、无上下文切换,低竞争场景纯用户态指令,比 synchronized 重量级锁的 OS 互斥量快
[拼多多]R3 #7025 CAS 和 synchronized 有什么区别?都用 synchronize 不行么?
答:CAS 乐观无锁,竞争弱时性能好;竞争激烈时 CAS 自旋耗 CPU,synchronized 挂起更划算
[蚂蚁]R2 #4114 synchronized/lock/cas 区别?
答:synchronized JVM内置阻塞,lock AQS阻塞可中断,CAS 硬件原子无锁自旋
[蚂蚁]R1 #6705 从 ConcurrentHashMap 一路问到锁优化、LongAdder、伪共享、缓存行填充、CAS
答:CHM 空桶 CAS 写入;LongAdder 分段 CAS 减冲突;伪共享靠缓存行填充避免;CAS 靠 lock+MESI
[美团]R4 #3974 AtomicInteger 实现原理?CAS 怎么实现原子操作?
答:Unsafe.getAndAddInt 自旋 CAS;CPU lock cmpxchg 单指令保证多核原子
[京东]R1 #3089 CAS 操作 ABA 问题?
答:Java 并发包提供 AtomicStampedReference,通过版本号保证 CAS 正确性
aba-problem
🎯 骨架
★ 现象:值从 A→B→A,CAS 看到 V==E 以为没变,实际中间被人动过
★ 通俗类比:包寄柜子里回来还在以为没人动,其实被打开拿走又换了个一样的回来
★ 解法:版本号机制——AtomicStampedReference(值+版本号)/ 数据库 version 字段 / AtomicMarkableReference(值+布尔标记)
★ 业务判断标准:是否依赖"值未变=没被动过"假设?依赖则必须解决(状态流转、风控联动、审计)
☆ 不需要解决的场景:普通计数加减、覆盖式更新、最终一致兜底(只关心最终值)
☆ 必须解决的场景:账单状态流转(待支付→支付中→待支付 可能误退款)、额度冻结+风控、审计轨迹
☆ AtomicStampedReference 内部:CAS 同时比较 value 和 stamp,二者都对才成功
⚠️ 易混淆
ABA 不是 CAS 独有,任何"只比较值不看历史"的乐观锁都有
ABA 解决方案是版本号,不是直接换悲观锁(只有竞争激烈时才换)
数据库的 version 字段就是 ABA 的工程化解法
AtomicReference 不解决 ABA,只是把对象 CAS;AtomicStampedReference 才解决
🎤 面经
[京东]R1 #3089 CAS 操作 ABA 问题?
答:值变 A→B→A,CAS 检测不到;用 AtomicStampedReference 通过版本号保证正确性
[快手]R1 #2673 乐观锁与悲观锁、CAS 的缺陷(ABA)?
答:CAS 自旋耗 CPU + ABA 问题 + 只能保证单变量;ABA 加版本号
[阿里]R2 #52 介绍下 CAS(追问 ABA 如何解决)
答:三元组比较交换;ABA 用 AtomicStampedReference 加版本号
[网易]R1 #1484 CAS 原理?ABA 问题怎么解决?
答:加版本号:每次 CAS 不仅比较值还比较版本,值相同版本不同也算变化
[Shopee]R1 #5398 什么是 ABA 问题,如何解决的?
答:值短暂变了又变回来 CAS 看不到;AtomicStampedReference 加版本号
[Shopee]R1 #791 JUC 原子类 ABA 问题:AtomicStampedReference 如何解决?电商订单状态变更能否用?
答:每次 CAS 比较 value+stamp 两元;订单状态流转必须用,否则可能误改回旧状态
[拼多多]R3 #1245 cas 怎么解决 ABA 问题?
答:AtomicStampedReference 版本号;或业务上数据库 version 字段
[京东]R3 #2063 CAS 会出现什么问题?ABA 怎么解决?
答:自旋空转/ABA/单变量;ABA 用版本号机制
volatile-jmm
🎯 骨架
★ JMM(Java Memory Model)= 主内存 + 每线程工作内存的抽象模型,规范多线程下的可见性、有序性、原子性
★ volatile 两个语义:可见性(写时刷主存 + 触发 MESI invalidate)+ 禁止重排序(内存屏障)
★ volatile 不保证原子性:i++ 是读-改-写三步,寄存器私有不受 MESI 管辖
★ 经典应用:DCL 单例必须加 volatile,防止 new 重排导致半初始化对象(分配内存→赋引用→初始化 这种乱序)
☆ 屏障插入:volatile 写前插 StoreStore、写后插 StoreLoad;读后插 LoadLoad+LoadStore
☆ x86 是 TSO 强模型,只有 StoreLoad 需要 mfence,其他三种是空操作;ARM/RISC-V 弱模型四种都要
☆ 用法判定:单次读/写 → volatile 够;复合操作 → 需 synchronized 或 CAS
⚠️ 易混淆
volatile 保证的是"每一步的可见性",不是"多步的原子性"——volatile int count; count++ 仍不安全
"volatile 让每次都从主内存读" 不准确,MESI 让其他核 Cache Line 变 Invalid,下次读重新加载
DCL 不加 volatile 出问题的根因是指令重排序,不是可见性
JMM ≠ JVM 内存结构(堆栈方法区),JMM 是并发语义模型
🎤 面经
[阿里]R1 #2951 JAVA 内存模型?
答:抽象的主存+工作内存模型,规范可见性/有序性/原子性,通过 happens-before 给程序员承诺
[阿里]R1 #2952 线程之间的通信方式,通过 volatile/synchronized/Lock,结合内存模型讲
答:共享变量+三种同步机制,volatile 走屏障+MESI,synchronized 走 monitor,Lock 走 AQS
[Shopee]R1 #1146 讲解一下 JMM 内存模型?
答:主内存存共享变量,每线程工作内存存副本,通过 8 种原子操作 + happens-before 规则保证可见性
[Shopee]R2 #1001 JMM 结构是什么样?(不是 jvm 内存结构)
答:主内存 + 工作内存的抽象,跟 JVM 堆栈无关;JMM 解决多线程语义,JVM 内存结构解决数据存放
[小米]R1 #3649 volatile 关键字的作用是什么?原理是什么?
答:可见性 + 禁止重排序;写时插 StoreLoad 屏障刷主存触发 MESI,读时强制从主存读
[百度]R2 #2818 volatile 怎么保证可见性和有序性的?
答:可见性靠 StoreLoad 屏障 + MESI;有序性靠四种屏障禁止重排
[百度]R2 #7150 i++ 是线程安全吗?加了 volatile 保证可见性,是线程安全吗?
答:不安全;volatile 只保证每步可见,不保证三步(读-改-写)原子,需 AtomicInteger
[拼多多]R3 #6939 volatile 的作用,保证可见性是指什么的可见性?
答:共享变量修改对其他线程立即可见,写主存+让其他核 Cache 失效
happens-before
🎯 骨架
★ 定义:JMM 给程序员的承诺——A happens-before B,则 A 的结果对 B 可见,且 A 的执行顺序排在 B 之前(语义上)
★ 核心价值:屏蔽 CPU 缓存 / 重排序细节,程序员只看 hb 规则就能写对并发代码
★ 8 条规则记住前 4 条:①程序顺序(同线程内)②volatile 写 hb 后续读 ③unlock hb 后续 lock ④start() hb 子线程任何操作
☆ 后 4 条:⑤线程终止 ⑥interrupt() hb 检测中断 ⑦构造完成 hb finalize ⑧传递性(A hb B, B hb C → A hb C)
☆ 与重排序的关系:满足 hb 的两个操作不能被重排(语义上);不满足的 JVM 可自由重排
☆ DCL 用 volatile 的本质:让"对象初始化"hb"引用赋值",禁止重排导致半初始化
⚠️ 易混淆
happens-before 不等于"时间上先发生",是"语义上的可见性顺序"
程序顺序规则只在同一线程内成立,跨线程必须靠其他规则
锁规则是 unlock hb 后续 lock,不是 lock hb unlock
volatile 写 hb 后续 volatile 读,写普通变量 → 读 volatile 不在此列
🎤 面经
[小红书]R1 #4107 讲几个 Happens-Before 原则?
答:程序顺序、volatile 写读、锁的 unlock-lock、线程 start-run,传递性
[拼多多]R3 #1275 happen-before 原则?
答:JMM 对程序员的承诺,A hb B 则 A 的结果对 B 可见;8 条规则核心是程序顺序+volatile+锁+线程 start
[百度]R2 #2818 volatile 怎么保证有序性的?
答:通过四种内存屏障建立 happens-before 关系,写前 StoreStore 写后 StoreLoad
[阿里]R1 #2952 结合内存模型讲线程通信
答:JMM 通过 happens-before 规则规范,volatile/synchronized/Lock 都建立 hb 关系保证可见性
[Shopee]R2 #1001 JMM 结构?
答:主存+工作内存,通过 happens-before 规则约束 8 种原子操作的可见性顺序
[Shopee]R1 #789 volatile 的局限性:为何 DCL 单例模式仍需 volatile?
答:建立"对象构造完成 hb 引用赋值"的语义,防止 new 三步骤被重排导致拿到半初始化对象
memory-barrier
🎯 骨架
★ 四种屏障:LoadLoad(读-读)/ StoreStore(写-写)/ LoadStore(读-写)/ StoreLoad(写-读,最重)
★ volatile 写:[StoreStore] → volatile写 → [StoreLoad];volatile 读:volatile读 → [LoadLoad] → [LoadStore]
★ StoreLoad 是"全能屏障"——同时禁止 Store 和 Load 重排,对应 x86 的 mfence 指令(也是开销最大的)
★ 屏障语义 ≠ MESI:屏障是 JVM 给 CPU 下的"命令",MESI 是 CPU 间缓存同步的"机制",二者协作实现可见性
☆ x86 TSO 模型:天然保证 LoadLoad/StoreStore/LoadStore,只有 StoreLoad 需要真正的 CPU 指令
☆ ARM/RISC-V 弱模型:四种屏障都需要显式插入(dmb / fence 指令)
☆ 屏障的本质:刷写缓冲(Store Buffer)+ 失效失效队列(Invalidate Queue),保证 CPU 间数据一致
⚠️ 易混淆
LoadLoad 屏障不负责可见性,只负责"防止读重排";可见性核心是 StoreLoad + MESI
"volatile 让每次从主存读"不准确,MESI 让其他核 Cache 失效后才重新读
屏障禁止的是"穿越屏障"的重排,不是禁止所有重排
mfence 不是唯一实现,lock addl $0x0, (%rsp) 也可以充当 StoreLoad
🎤 面经
[滴滴]R3 #2549 volatile 关键字介绍,内存屏障有哪些?
答:四种:LoadLoad/StoreStore/LoadStore/StoreLoad;volatile 写前 StoreStore,写后 StoreLoad
[Shopee]R3 #889 volatile 有什么用,内存屏障怎么实现的?
答:JVM 在字节码层插入屏障,编译为 CPU 的 mfence/lfence/sfence 指令;x86 上只有 StoreLoad 需要真正指令
[百度]R2 #2818 volatile 怎么保证有序性的?
答:通过四种内存屏障禁止重排序:写前 StoreStore、写后 StoreLoad、读后 LoadLoad+LoadStore
[小红书]R1 #4107 Happens-Before 原则?
答:屏障是 hb 的底层实现,volatile 写读靠屏障建立 hb 关系
[滴滴]R4 #5451 volatile 关键字的作用以及实现原理?
答:可见性 + 禁止重排,靠内存屏障;写后 StoreLoad 强制刷主存 + 让其他核失效缓存
optimistic-pessimistic-lock
🎯 骨架
★ 悲观锁:假设一定有冲突,先加锁再操作(synchronized、ReentrantLock、SELECT...FOR UPDATE)
★ 乐观锁:假设没冲突,操作时验证(CAS、version 字段、时间戳)
★ 选择标准:读多写少 / 竞争弱 → 乐观锁;写多读少 / 竞争激烈 → 悲观锁(避免自旋空转)
★ 数据库实现:悲观锁 = SELECT ... FOR UPDATE(行锁);乐观锁 = UPDATE ... WHERE version = ?
☆ Java 实现:悲观锁 = synchronized/Lock;乐观锁 = Atomic 系列(底层 CAS)/ LongAdder
☆ 乐观锁三大坑:ABA、自旋空转、只能保证单变量原子
☆ 隐式悲观/乐观:MySQL InnoDB 的 RR 隔离 = 当前读悲观(行锁/间隙锁),快照读乐观(MVCC 不阻塞读)
⚠️ 易混淆
乐观锁 ≠ 没有锁,是用 CAS/version 替代加锁,实质上还是冲突检测
悲观锁不是"很差",竞争激烈时反而比乐观锁强(避免自旋)
SELECT FOR UPDATE 是悲观锁;SELECT ... LOCK IN SHARE MODE 是共享悲观锁
分布式场景悲观锁通常用 Redis/Zookeeper,乐观锁用 version 字段
🎤 面经
[蚂蚁]R1 #6740 乐观锁和悲观锁的区别?这两种锁在 Java 和 MySQL 分别是怎么实现的?
答:悲观假设有冲突先加锁;乐观假设无冲突再验证。Java:synchronized/Lock vs CAS。MySQL:FOR UPDATE vs version
[蚂蚁]R1 #6819 乐观锁和悲观锁。可重入锁和 Synchronized
答:选择标准:读多写少乐观,写多读少悲观;synchronized 是可重入悲观锁
[Shopee]R1 #2772 乐观锁与悲观锁的区别?
答:悲观先锁后操作(synchronized/FOR UPDATE);乐观先操作后验证(CAS/version)
[快手]R2 #6373 数据库的乐观锁和悲观锁?
答:悲观:SELECT FOR UPDATE 加行锁;乐观:version 字段 + UPDATE WHERE version=?
[蚂蚁]R4 #3445 乐观锁和悲观锁你是怎么选择的?
答:看冲突概率:低用乐观(开销小),高用悲观(避免自旋空转);写多读少必悲观
[京东]R3 #3081 说下乐观锁,悲观锁(select for update),并写出 SQL 实现?
答:悲观:SELECT * FROM t WHERE id=1 FOR UPDATE;乐观:UPDATE t SET v=v+1, version=version+1 WHERE id=1 AND version=?
[蚂蚁]R1 #6903 加锁有几种方式,悲观锁乐观锁?
答:悲观:synchronized/Lock/FOR UPDATE;乐观:CAS/version;分布式 Redis/ZK
[快手]R1 #2673 乐观锁与悲观锁、CAS 的缺陷?
答:CAS 是乐观锁基础,缺陷:ABA、自旋空转、单变量限制
S3 锁的工程化
aqs-principle
🎯 骨架
★ AQS(AbstractQueuedSynchronizer)= volatile int state(锁状态)+ CLH 双向链表(等待队列)+ exclusiveOwnerThread
★ 获取锁:tryAcquire(CAS 抢) → 失败 addWaiter(入队) → acquireQueued(自旋+park 阻塞)
★ 释放锁:tryRelease(state-1) → state==0 时 unparkSuccessor 唤醒 head.next
★ FIFO 公平性:只有"前驱是 head"的节点才能尝试获取锁,避免队列中间节点乱抢
★ 双向链表原因:unparkSuccessor 时如果 next=null(addWaiter 先连 prev 后连 next),需从 tail 往前遍历找有效后继
☆ 两种模式:独占(ReentrantLock)/ 共享(Semaphore/CountDownLatch/读锁)
☆ 子类只需重写 tryAcquire/tryRelease,队列管理由 AQS 框架完成(模板方法)
☆ waitStatus:SIGNAL=-1(前驱释放时叫醒我)/ CANCELLED=1(已取消)/ 0=初始
⚠️ 易混淆
AQS 不只是 ReentrantLock,是整个 JUC 锁框架的基础(Semaphore/CountDownLatch/ReadWriteLock 都基于它)
head 是哨兵节点(dummy),head.next 才是真正第一个等待线程
独占锁释放时只唤醒一个(无惊群),共享锁会传播唤醒(部分惊群但有控制)
ReentrantLock 用 AQS state 记录重入次数,不是 0/1
🎤 面经
[快手]R2 #2749 AQS 了解吗?怎么实现公平锁、非公平锁?
答:state+CLH 队列;公平锁多 hasQueuedPredecessors 检查,有人排队就不抢
[拼多多]R3 #6936 AQS 底层?
答:volatile int state + CLH 双向链表 + CAS;模板方法子类重写 tryAcquire/tryRelease
[快手]R1 #2728 讲讲 java 锁,锁升级以及 reentrantlock,然后让讲 aqs
答:AQS 是 ReentrantLock 等的底层框架,state 记录锁状态,队列存等待线程
[蚂蚁]R2 #4115 aqs 原理?
答:抽象队列同步器,state+CLH 队列+CAS,所有 JUC 锁的基石
[小红书]R1 #4074 Lock 的底层数据结构?怎么实现公平与非公平?队列里的节点存什么?
答:双向链表节点,存 thread/前驱后继/waitStatus;公平多 hasQueuedPredecessors 检查
[阿里]R4 #6607 aqs 是啥?
答:AbstractQueuedSynchronizer,并发包的锁框架基石,state 表示状态,队列管等待线程
[美团]R1 #4944 AQS 原理,java 中封装了哪些?
答:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier 都基于 AQS
[京东]R1 #4013 sync 锁和 lock 的区别?aqs 的原理?
答:AQS 用 state 标记锁,CLH 队列存等待线程,CAS 抢锁失败入队 park
[快手]R2 #6375 AQS 原理详细介绍?
答:三大件:state(int 锁状态)+ CLH 队列(双向链表)+ CAS(无锁原语);获取/释放走模板方法
reentrantlock-mechanism
🎯 骨架
★ 基于 AQS 实现,state 表示锁状态:0=无锁,>0=重入次数
★ 可重入:同一线程再次获取 state++,释放 state--,归零才真正释放
★ 比 synchronized 多四大功能:公平锁(构造传 true)、可中断(lockInterruptibly)、超时获取(tryLock(timeout))、多 Condition
★ 必须 finally unlock:异常时锁不释放会死锁;与 synchronized 自动释放对比的关键差异
★ 公平 vs 非公平:核心差异就一行 hasQueuedPredecessors()——有人排队就不抢
☆ 非公平锁吞吐高 5-10 倍:减少线程切换 + CPU 缓存局部性;新线程直接抢省 park/unpark 开销
☆ 非公平锁两次插队:lock() 入口直接 CAS + tryAcquire 里再 CAS
☆ 选型:简单场景 synchronized(自动释放安全);要超时/可中断/多条件就 ReentrantLock
⚠️ 易混淆
ReentrantLock 必须配 try-finally,忘 unlock 直接死锁;synchronized 不会
公平锁不是"绝对公平",是"按入队顺序";插队的成功后队列不变
性能 ReentrantLock 不一定比 synchronized 快,JDK 6 后 synchronized 锁升级优化后接近
"可重入"指同线程重入,不是不同线程
🎤 面经
[腾讯]R2 #3120 Synchronized 和 ReentrantLock 区别?
答:JVM 内置 vs AQS API;自动释放 vs 手动;不支持公平/中断/超时 vs 全支持
[滴滴]R3 #2983 ReentrantLock 如何实现超时锁的等待?
答:tryLock(timeout, unit) 调 AQS 的 doAcquireNanos,超时返回 false 不阻塞
[蚂蚁]R1 #6712 用过哪些锁,synchronized ReentrantLock。可重入锁是什么,如何实现?
答:ReentrantLock 用 AQS state 记录重入次数,同线程再次获取 state++
[腾讯]R1 #7056 java 有哪些锁?ReentrantLock 是怎么保证线程安全的?
答:基于 AQS:volatile state + CAS 抢锁失败入 CLH 队列 park 阻塞
[字节]R1 #4825 synchronized 和 ReentrantLock 实现和区别,jvm 层的字节码?
答:synchronized 编译后 monitorenter/exit;ReentrantLock 是普通 Java 类调 Unsafe
[B站]R1 #2760 可重入锁 reentrantlock;volatile 和 transient 关键字
答:同线程多次获取无问题,state 计数;与 synchronized 都可重入
[快手]R2 #6375 公平锁和非公平锁,底层怎么实现的?
答:公平锁多 hasQueuedPredecessors 检查;非公平锁直接 CAS 抢
[携程]R1 #2893 synchronize 互斥锁 ReentrantLock 可重入锁(自旋锁)
答:都可重入;自旋指 CAS 失败重试,不是真意义的自旋锁(重量级锁会 park 阻塞)
[网易]R1 #3226 手写死锁(Synchronized 和 ReentrantLock)?
答:两线程各持一锁请求另一锁;ReentrantLock 用 tryLock(timeout) 可主动避免
readwrite-lock
🎯 骨架
★ 适用读多写少场景:读读并发、读写互斥、写写互斥(读多了浪费 ReentrantLock 的独占)
★ AQS state 拆分:高 16 位 = 读锁计数(共享)/ 低 16 位 = 写锁计数(独占)
★ 读锁获取:写锁未持有 → CAS 增加高 16 位(不同读线程都能成功)
★ 写锁获取:读锁和写锁都未持有 → CAS 设置低 16 位
★ 锁降级 OK:持有写锁可获取读锁→释放写锁(写→读降级),保证读的连续性
★ 锁升级禁止:持有读锁不能升写锁(多个读线程都试图升会死锁)
☆ 写饥饿问题:非公平模式下读线程持续到来写线程一直等不到;解法用公平模式或 StampedLock
☆ StampedLock(JDK 8):乐观读模式不阻塞写,validate() 失败再退化为悲观读
⚠️ 易混淆
ReadWriteLock 不能锁升级(容易死锁),只能锁降级
公平模式下读锁也要排队,不能插队
一个 state 同时存读写计数靠位运算(高 16 / 低 16),不是两个变量
StampedLock 乐观读期间,写可以进行,validate 失败就退化为悲观
🎤 面经
[腾讯]R1 #2193 读写锁了解么?它是怎么解决你刚才说的那个公平的问题呢?
答:读读并发写写互斥;公平模式下读写按 FIFO 排队,避免写饥饿
[京东]R1 #1818 JDK 读写锁?
答:ReentrantReadWriteLock,state 高 16 位记读锁、低 16 位记写锁;读多写少场景比 ReentrantLock 吞吐高
[蚂蚁]R1 #6705 从 ConcurrentHashMap 一路问到锁优化、LongAdder、伪共享、缓存行填充、CAS?
答:锁优化路径:粗→细→分段→读写分离→无锁;读写锁是从独占到读共享的关键一步
[滴滴]R1 #2394 Java 里都有哪些锁,针对各种场景,对锁进行了哪些优化?
答:读写锁是读多写少场景的优化;state 一字两段;锁降级允许(写→读),锁升级禁止
[携程]R2 #1881 锁优化?
答:锁优化思路:缩小粒度→分段→读写分离→无锁;读写锁是 ReentrantLock 在读多写少场景的针对性优化
[蚂蚁]R1 #4103 考虑 jdk 17 的优化手段最大限度的支持并发,以及可重入锁的设计?
答:读写锁 + StampedLock 乐观读;可重入靠 state 计数+ownerThread 比较
condition-mechanism
🎯 骨架
★ Condition = Lock 版的 wait/notify,由 lock.newCondition() 创建,必须在 lock 持有期间调用
★ 比 wait/notify 强:可创建多个 Condition(如 notFull / notEmpty),分类管理等待线程
★ await():释放锁 + 加入"条件队列" + park 阻塞;可中断、可超时(awaitNanos)
★ signal():从条件队列移到 AQS 同步队列(不是直接获取锁),需重新竞争锁
★ 经典应用:ArrayBlockingQueue 用两个 Condition——队列空时 take 在 notEmpty 等,put 后 notEmpty.signal
☆ 实现原理:AQS 内部维护单向条件队列(区别于同步队列双向),await 时移出同步队列入条件队列;signal 时反向
☆ 与 wait/notify 的设计差异:wait/notify 共用一个 WaitSet 谁都收到通知;Condition 分队列精确通知
⚠️ 易混淆
await 必须在 lock.lock() 持有锁后调用,否则抛 IllegalMonitorStateException
signal 不会立即让被唤醒线程执行,需要先重新拿到锁
多个 Condition 是为了"分类等待"——例如生产者-消费者用 notFull/notEmpty 减少无效唤醒
条件队列和 AQS 同步队列是两个不同结构,await 切换队列
🎤 面经
[蚂蚁]R1 #6730 阻塞队列不用 java 提供的自己怎么实现,condition 和 wait 不能用?
答:ReentrantLock + 双 Condition:notFull(put 等满)和 notEmpty(take 等空);用 LockSupport.park/unpark 也行
[美团]R4 #6984 AQS、重入锁、Condition?
答:Condition 基于 AQS 条件队列,await 释放锁入条件队列 park,signal 反向
[网易]R2 #4231 Lock 的加锁和解锁过程,公平/非公平实现,Condition 源码?
答:Condition 内部 ConditionObject,await 时 fullyRelease 释放锁加入条件队列 park
[蚂蚁]R1 #6791 你们项目中哪些地方用到了多线程?涉及 BlockingQueue。它的特点是什么?
答:阻塞队列内部用 ReentrantLock + Condition(notFull/notEmpty)实现 put/take 阻塞唤醒
[Shopee]R1 #792 阻塞队列选型:ArrayBlockingQueue vs LinkedBlockingQueue,电商秒杀场景如何选择?
答:ArrayBlockingQueue 用同一把锁配双 Condition;秒杀场景用有界队列防积压
[快手]R2 #6319 手写 semaphore,使用 object 中的 wait 和 notify 方法?
答:也可用 Lock+Condition:count>0 不等,count==0 在 Condition 上 await,release 时 signal
lock-interface
🎯 骨架
★ 6 个核心方法:lock() / lockInterruptibly() / tryLock() / tryLock(timeout) / unlock() / newCondition()
★ lock():阻塞获取,不响应中断(即使 interrupt 也继续等)
★ lockInterruptibly():阻塞但响应中断,被 interrupt 抛 InterruptedException
★ tryLock():非阻塞立即返回 true/false;tryLock(timeout):最多等指定时间
★ unlock() 必须在 finally:异常时锁释放,避免死锁
★ 比 synchronized 多的能力:超时(避免死锁)、可中断(避免永久阻塞)、多 Condition(细粒度等待队列)
☆ tryLock 三大场景:①避免死锁(拿不到就放弃做兜底)②限时操作(资源紧时快速失败)③非阻塞算法
☆ Lock 是接口,主要实现:ReentrantLock / ReentrantReadWriteLock.ReadLock/WriteLock / StampedLock 不实现 Lock 接口
⚠️ 易混淆
Lock 是接口(java.util.concurrent.locks.Lock),ReentrantLock 是它的实现
tryLock() 不传时间是非阻塞,传时间是定时阻塞
lockInterruptibly 抛 InterruptedException,需要 try-catch
StampedLock 不实现 Lock 接口(设计有意,因为有乐观读特殊语义)
🎤 面经
[腾讯]R2 #3120 Synchronized 和 ReentrantLock 区别?
答:Lock 接口提供 tryLock/lockInterruptibly/超时获取/多 Condition,synchronized 都不支持
[滴滴]R3 #2983 Java 锁有了解吗?ReentrantLock 如何实现超时锁的等待?
答:用 Lock 接口的 tryLock(timeout, unit),底层 AQS 的 doAcquireNanos 自旋+park 直到超时
[快手]R2 #6375 synchronized 与 Lock 有哪些区别?
答:Lock 是 API 级(手动 lock/unlock),灵活但易出错;synchronized JVM 级,自动释放
[字节]R1 #4825 synchronized 和 ReentrantLock 实现和区别?
答:Lock 接口规范出 4 种获取方式(普通/可中断/非阻塞/超时),都靠 AQS 实现
[携程]R3 #2934 Synchronized 和 ReentrantLock 区别?
答:Lock 多 4 个能力:tryLock/lockInterruptibly/超时/多 Condition
[蚂蚁]R1 #6730 阻塞队列不用 java 提供的自己怎么实现,condition 和 wait 不能用?
答:Lock 接口的 newCondition() 可以创建多个等待队列,是阻塞队列的核心
[百度]R1 #2486 lock 与 tryLock 的区别?
答:lock 阻塞获取不返回;tryLock 立即返回 true/false;tryLock(timeout) 限时阻塞
[蚂蚁]R1 #6903 加锁有几种方式?
答:synchronized/ReentrantLock(实现 Lock 接口)/分布式锁;Lock 接口规范了 lock/tryLock/lockInterruptibly 等方法
S4 线程池与任务调度
threadpool-core-params
🎯 骨架
★ 7 个参数:corePoolSize / maximumPoolSize / keepAliveTime / unit / workQueue / threadFactory / handler
★ corePoolSize:核心线程数,常驻不回收(除非 allowCoreThreadTimeOut=true)
★ maximumPoolSize:最大线程数,核心+非核心总上限
★ keepAliveTime:非核心线程空闲存活时间,超时被回收
★ workQueue:任务队列,常用 LinkedBlockingQueue(无界,慎用)/ ArrayBlockingQueue(有界)/ SynchronousQueue(不存任务直接交接)
★ handler:拒绝策略,队列满+线程满时触发(4 种内置 + 自定义)
☆ threadFactory:自定义线程名(生产必须,方便排查)/ 守护线程标记 / 优先级
☆ 提交执行顺序:核心线程→入队→非核心线程→拒绝(这个顺序是 Doug Lea 选的,资源效率优先;Tomcat 反过来响应优先)
⚠️ 易混淆
默认情况核心线程不回收(即使闲置),需要 allowCoreThreadTimeOut(true) 才回收
maximumPoolSize 是总上限(含 core),不是"额外"
队列满 ≠ 立即拒绝,先扩到 maximumPoolSize 才拒绝
LinkedBlockingQueue 默认无界(Integer.MAX_VALUE),用了可能 OOM
🎤 面经
[携程]R1 #2889 线程池参数:corePoolSize/maximumPoolSize/RejectPolicy/keepAliveTime/ThreadFactory/workQueue/unit
答:7 个参数对应:常驻线程数/上限/拒绝策略/存活时间/线程工厂/任务队列/时间单位
[滴滴]R1 #3025 线程池参数,怎么确定核心线程大小?
答:7 个参数;核心线程数 = CPU 密集 N+1,IO 密集 N×2,需压测调优
[蚂蚁]R1 #6838 Java 初始化一个线程池有哪些参数可以配置,分别是什么作用?
答:corePoolSize 常驻线程;maxPoolSize 上限;keepAliveTime 非核心闲置存活;workQueue 任务队列;threadFactory 线程工厂;handler 拒绝策略
[快手]R1 #2739 线程池的核心线程数和最大线程数的作用;工作队列和最大线程数有没有关系?
答:core 常驻处理稳定流量,max 应对突发;队列满才扩到 max(先入队后扩容)
[携程]R1 #2942 线程池参数合理,考虑实际场景?
答:不能拍脑袋设;先测业务 QPS+RT,按公式起步,监控队列深度+拒绝率动态调
[京东]R1 #5477 java 线程池参数。举例一个业务场景和你的线程池设计方案
答:7 参数;电商订单异步:core=10 max=20 队列 ArrayBlockingQueue(100) 拒绝策略 CallerRuns
[蚂蚁]R1 #6711 什么时候到达最大线程数 到达最大线程后继续提交的表现?
答:核心满+队列满后才扩到 max;max 后继续提交触发拒绝策略
[蚂蚁]R1 #6880 线程池参数?
答:7 个:核心线程数/最大线程数/存活时间/单位/队列/线程工厂/拒绝策略
[蚂蚁]R1 #4092 核心参数配置,相关注意点?
答:核心数按 CPU/IO 类型设;队列要有界防 OOM;线程工厂自定义命名方便排查
threadpool-reject-policy
🎯 骨架
★ 触发条件:workQueue 满 + workerCount >= maximumPoolSize 同时成立时才触发
★ 4 种内置策略:
AbortPolicy(默认):抛 RejectedExecutionException
CallerRunsPolicy:调用者线程亲自执行(降速但不丢任务)
DiscardPolicy:静默丢弃,无任何提示
DiscardOldestPolicy:丢队列里最老任务,再尝试入队
★ AbortPolicy 的坑:调用方未捕获异常 → 整个调用链失败;适合任务可丢弃场景
★ CallerRunsPolicy 的坑:阻塞提交线程(如 Tomcat HTTP 线程),可能拖垮整个服务
★ 生产实践:自定义 RejectedExecutionHandler——记日志 + 降级(持久化到 DB/MQ)+ 告警
☆ 选择参考:丢不起 → 自定义/CallerRuns;可丢 → DiscardOldest(队列滚动);告警优先 → AbortPolicy
☆ 实际故障:xx 项目用 AbortPolicy,商户增 19.5 倍打满线程池影响 14W+ 用户
⚠️ 易混淆
拒绝触发 = 队列满 AND 线程满,单条件不会触发
CallerRunsPolicy 不阻塞线程池,但阻塞调用者;调用者是 Tomcat 线程时危险
DiscardPolicy 静默丢弃没日志,生产严禁用
自定义拒绝策略只需实现 RejectedExecutionHandler 接口的 rejectedExecution 方法
🎤 面经
[字节]R1 #7311 线程池的拒绝策略有哪几种?什么时候用哪种?
答:4 种:Abort(默认抛异常)/CallerRuns(降速)/Discard(静默丢)/DiscardOldest(丢老任务);按是否能丢选
[蚂蚁]R1 #6662 线程池的增长策略和拒绝策略,几种拒绝策略?
答:增长:核心→队列→非核心;拒绝 4 种内置;高并发用 CallerRuns 自我保护
[蚂蚁]R1 #4211 一个核心线程数 3,最大线程数 5,拒绝策略为直接丢弃,放进 100 个任务,哪些执行/阻塞/丢弃?
答:3 个执行(核心)+ 队列容量(默认 LinkedBlockingQueue 无界但题里假设有限)+ 2 个非核心;超出全部 Discard
[快手]R1 #2739 线程池的拒绝策略?
答:AbortPolicy 抛异常 / CallerRunsPolicy 调用者运行 / DiscardPolicy 丢弃 / DiscardOldestPolicy 丢老的
[携程]R3 #2932 线程池参数 拒绝策略有哪些 同步队列哪几种?区别?
答:拒绝 4 种;队列:LinkedBlockingQueue(无界链表)/ArrayBlockingQueue(有界数组)/SynchronousQueue(不存任务直接交接)
[滴滴]R1 #3015 并发量大触发拒绝策略怎么解决?保证不丢且量大用什么替代?
答:自定义拒绝策略持久化到 DB/MQ;不丢且量大用 MQ 削峰填谷替代纯线程池
[蚂蚁]R4 #4179 线程池由哪些组件组成?拒绝策略有哪些?
答:ThreadPoolExecutor+Worker+BlockingQueue+RejectedExecutionHandler;4 种拒绝
[百度]R1 #2867 线程池的拒绝策略有哪些?
答:4 种内置;生产常自定义:记日志+降级+告警
threadpool-execution-flow
🎯 骨架
★ execute() 三个 if 分支(顺序很关键):
① workerCount < corePoolSize → addWorker(task, true) 创建核心线程
② workQueue.offer(task) → 任务入队 + 二次检查线程池状态
③ addWorker(task, false) → 创建非核心线程,失败 → reject()
★ 顺序记忆:"核心 → 队列 → 非核心 → 拒绝",先复用再扩容(Tomcat 反过来)
★ Worker 复用:runWorker() 里 while 循环,getTask() 从队列取下一个任务执行
★ 核心线程保活:getTask() 调 workQueue.take() 阻塞等待(OS park 不耗 CPU)
★ 非核心回收:getTask() 调 workQueue.poll(keepAliveTime),超时返回 null → 线程退出
☆ Worker 继承 AQS:实现不可重入锁,防止 interruptIdleWorkers 中断正在执行任务的线程
☆ submit vs execute:submit 包装 FutureTask 后调 execute,多了返回 Future;submit 异常被 Future 捕获,不会传播
☆ ctl 字段:AtomicInteger 高 3 位存状态、低 29 位存线程数,一次 CAS 同时更新
⚠️ 易混淆
不是核心满了立即扩容到 max,而是先入队,队列满才扩
核心线程不"被回收",除非 allowCoreThreadTimeOut(true)
submit 抛的异常会吞,需要 future.get() 才能拿到(坑点)
take() 不耗 CPU 是因为线程被 OS 移出调度队列(WAITING 状态)
🎤 面经
[字节]R1 #4882 线程池是如何创建的,用了哪些参数,如何动态修改?线程池不同状态的转移?
答:ThreadPoolExecutor 7 参数构造;setCorePoolSize/setMaximumPoolSize 动态调;状态 RUNNING→SHUTDOWN→STOP→TIDYING→TERMINATED
[阿里]R1 #2949 线程池(具体参数,拒绝策略,减少线程的机制,线程复用的原理)?
答:复用:runWorker while 循环 getTask 阻塞等下个任务;减线程:非核心 poll(keepAlive) 超时退出
[蚂蚁]R1 #3478 线程池实现原理,达到最大数量后续 submit 会怎么样?
答:核心→队列→非核心三级递进;满了触发拒绝策略,AbortPolicy 抛 RejectedExecutionException
[蚂蚁]R1 #6884 线程池,execute 和 submit 的区别?
答:submit 包装 FutureTask 返回 Future;execute 无返回;submit 异常被 Future 吞,需 future.get()
[百度]R1 #2867 线程池 execute 和 submit 的区别?
答:同上;execute 适合 fire-and-forget,submit 适合需要结果或异常处理
[快手]R1 #2741 线程池的工作原理?为什么最大线程数设置为核心线程数的 2 倍?
答:三个 if 分支;2 倍是 IO 密集型经验值(IO 等待时多开线程利用 CPU)
[百度]R1 #2855 分析线程池的实现原理和线程的调度过程?
答:execute→addWorker→runWorker→getTask 循环;调度由 OS 完成,线程池只管复用
[滴滴]R1 #3015 线程池的实现原理。并发量大触发拒绝策略怎么解决?
答:核心→队列→非核心;拒绝时降级或用 MQ 削峰
[拼多多]R3 #6937 线程池底层?
答:ThreadPoolExecutor + Worker(AQS) + BlockingQueue;Worker 不可重入锁防中断
threadpool-sizing
🎯 骨架
★ CPU 密集型:coreSize = N核 + 1(+1 是利用某个线程偶尔阻塞的空隙时间片)
★ IO 密集型:coreSize = N核 × 2 或 N核 / (1 - IO等待比)(经验公式,IO 等待时 CPU 闲)
★ 实际开发:公式只是起点,必须压测——QPS、RT、队列深度、CPU 利用率综合调
★ 监控指标四件套:activeCount(活跃线程)/ queueSize(队列积压)/ completedTaskCount / rejectedCount
★ 队列容量也要算:估算"瞬时突增请求量 × RT"作为队列容量上限
☆ 混合型任务:拆成两个池(IO 池 + CPU 池),避免相互拖累
☆ 阿里规范:禁用 Executors.newXxx,因为底层用无界队列(OOM 隐患)
☆ 反模式:所有线程池都设 coreSize=CPU 核数(CPU/IO 不区分会导致 IO 池吞吐低 50%)
⚠️ 易混淆
"核数 + 1"是 CPU 密集,不是通用公式
公式不是死的,要看任务平均阻塞时间——阻塞越多,线程数越大
队列大小不是越大越好——任务排队太久会超时失败,相当于变相拒绝
单进程线程数有上限(OS 资源 + 内存栈空间),通常不超过几百
🎤 面经
[拼多多]R3 #6938 如何确定线程数设置多少?
答:CPU 密集 N+1;IO 密集 N×2;按业务压测调,不能拍脑袋
[快手]R1 #2741 为什么最大线程数设置为核心线程数的 2 倍?
答:IO 密集型经验:IO 等待时 CPU 空闲,2 倍核数能利用空闲时间,平衡上下文切换开销
[滴滴]R1 #3025 线程池参数,怎么确定核心线程大小?
答:区分 CPU/IO 密集;先按公式起步,监控队列深度和拒绝率压测调优
[携程]R1 #2942 线程池参数合理,考虑实际场景?
答:不能拍脑袋;监控指标 + 压测 + 动态线程池配置中心调
[蚂蚁]R1 #4092 核心参数配置,相关注意点?
答:看任务类型设核数;队列必须有界防 OOM;注意 Tomcat HTTP 线程不要阻塞
[蚂蚁]R1 #6662 高并发情况下,如何使用线程池?
答:核心数按业务起步 + 配合 MQ 削峰(线程池本身不能扛海量请求)
[京东]R1 #5477 举例一个业务场景和你的线程池设计方案?
答:业务 QPS×平均RT 估算;CPU N+1 / IO N×2 起步;监控队列深度 + 拒绝率压测调优
[字节]R1 #4882 线程池如何动态修改?
答:setCorePoolSize/setMaximumPoolSize 运行时调整;配合 Nacos/Apollo 监听
no-executors
🎯 骨架
★ 阿里规范禁用 Executors,必须手动 new ThreadPoolExecutor:明确 7 个参数特别是有界队列和拒绝策略
★ newFixedThreadPool / newSingleThreadExecutor:底层 LinkedBlockingQueue 无界(capacity = Integer.MAX_VALUE)→ 任务堆积 OOM
★ newCachedThreadPool:maximumPoolSize = Integer.MAX_VALUE → 线程数爆炸 OOM;用 SynchronousQueue 不存任务
★ newScheduledThreadPool:DelayedWorkQueue 也是无界 → 同样 OOM 风险
★ 正确做法:手动 new ThreadPoolExecutor,参数全显式:
队列:ArrayBlockingQueue 或限制 capacity 的 LinkedBlockingQueue
拒绝策略:根据业务选 / 自定义
threadFactory:自定义线程名(生产必须)
☆ 真实故障场景:低 QPS 时一切正常,流量翻几倍后队列积压 → JVM OOM 服务雪崩
☆ Spring 的 ThreadPoolTaskExecutor 也要小心:默认队列也无界,要 setQueueCapacity
⚠️ 易混淆
newCachedThreadPool 不是"无队列",是 SynchronousQueue 不缓存任务,但线程数无上限
LinkedBlockingQueue 不是默认有界,默认 Integer.MAX_VALUE
阿里规范是"强制"不是"建议",code review 必查
newSingleThreadExecutor 同样问题(一个线程的 FixedThreadPool)
🎤 面经
[阿里]R5 #198 线程池实现方式-销毁线程?
答:不要用 Executors,自己 new ThreadPoolExecutor 显式参数;销毁靠 shutdown/shutdownNow
[阿里]R4 #7094 Executors 和 ThreadPoolExecutor 之间的关系?
答:Executors 是工厂封装,底层都是 ThreadPoolExecutor;阿里禁用工厂方法,因为参数不安全
[小米]R3 #7227 线程池有了解吗。有哪几种线程池?
答:4 种工厂方法:Fixed/Cached/SingleThread/Scheduled;都有 OOM 风险,生产用自定义 ThreadPoolExecutor
[百度]R1 #2867 线程池有哪几种,优劣是啥?
答:Fixed 队列无界 / Cached 线程数无界 / SingleThread 队列无界 / Scheduled 队列无界;都有 OOM 隐患
[蚂蚁]R1 #6662 高并发情况下如何使用线程池,用哪个?
答:不用 Executors;自定义 ThreadPoolExecutor + 有界队列 + 合理拒绝策略 + 配合 MQ 削峰
[快手]R2 #6376 单线程池和固定线程池为什么用 LinkedBlockingQueue,缓存线程池用 SynchronousQueue?
答:Fixed 无界 LBQ 任务堆积 OOM;Cached SynchronousQueue 不存任务但 maxPool=Integer.MAX_VALUE 线程爆炸 OOM
[小米]R3 #7227 线程池有哪几种?线程池的优点?
答:4 种工厂方法都有 OOM 隐患;优点是复用,但生产必须自定义 ThreadPoolExecutor
[百度]R1 #2867 线程池有哪几种,优劣是啥?
答:Fixed/Cached/Single/Scheduled,优势复用;劣势所有工厂方法都用无界队列或无上限线程数
threadpool-state
🎯 骨架
★ 5 种状态:RUNNING(-1) → SHUTDOWN(0) → STOP(1) → TIDYING(2) → TERMINATED(3),单向不可逆
★ RUNNING:接受新任务 + 处理队列(默认初始状态)
★ SHUTDOWN:拒绝新任务 + 继续处理队列(shutdown() 触发)
★ STOP:拒绝新任务 + 不处理队列 + 中断所有线程(shutdownNow() 触发)
★ TIDYING:所有任务完成 + workerCount=0,即将执行 terminated() 钩子
★ TERMINATED:terminated() 执行完毕,终态
☆ 状态用 ctl 高 3 位编码:RUNNING=-1(高位 111),其他从 000 递增;低 29 位存 workerCount
☆ 优雅关闭三步:shutdown() → awaitTermination(timeout) → 超时再 shutdownNow()
☆ shutdownNow 不能强杀线程,只发 interrupt 信号;不响应中断的任务(纯 CPU 计算)照样跑
⚠️ 易混淆
shutdown() 是非阻塞的,调用后立即返回,不等任务完成
shutdownNow() 返回 List(队列中未执行任务),需要持久化避免丢失
状态转移单向:RUNNING→SHUTDOWN→TIDYING 或 RUNNING→STOP→TIDYING,不可回退
TIDYING 是过渡态,调 terminated() 钩子后立即进 TERMINATED
🎤 面经
[字节]R1 #4882 线程池不同状态的转移?为什么使用线程池,和直接创建线程的优势?
答:5 状态单向转移:RUNNING→SHUTDOWN→STOP→TIDYING→TERMINATED;优势:复用减创建开销/限流防 OOM
[蚂蚁]R1 #3478 线程池达到最大数量后续 submit 的新任务会怎么样?
答:状态仍是 RUNNING,触发拒绝策略;不会自动切换状态
[快手]R2 #6376 单线程池和固定线程池为什么用 LinkedBlockingQueue,缓存线程池用 SynchronousQueue?
答:Fixed/Single 线程数固定靠队列缓冲;Cached 不缓存任务靠新建线程,关闭时影响状态转移
[阿里]R5 #198 线程池实现方式-销毁线程?
答:shutdown 触发 SHUTDOWN 状态,工作线程退出后进 TIDYING,最终 TERMINATED
[阿里]R4 #7094 线程池回收 / Executors 与 ThreadPoolExecutor 之间的关系?
答:回收靠状态转移,shutdown→awaitTermination→shutdownNow;Executors 是工厂方法
[百度]R1 #2867 线程池有哪几种?execute 和 submit 区别?
答:4 种工厂;状态机一致;execute 无返回,submit 返回 Future
[腾讯]R5 #7127 Java 的线程池,为什么要有线程池?
答:复用线程减开销+管理生命周期+统一配置;状态机控制可控关闭
[拼多多]R3 #6937 线程池底层?
答:ThreadPoolExecutor + Worker(AQS) + ctl(AtomicInteger 高 3 位状态低 29 位线程数)
forkjoinpool
🎯 骨架
★ 分治框架:大任务 fork 拆成小任务并发执行,最后 join 合并结果(适合递归可拆分场景)
★ Work-Stealing(工作窃取):每个线程有自己的双端队列 WorkQueue,空闲线程从其他线程队列偷任务
★ 自己取尾部(LIFO):新 fork 的小任务从尾部 push,自己 pop,缓存热+快速完成
★ 偷别人头部(FIFO):偷的是头部大任务(父任务),一次偷够,减少偷窃竞争
★ ForkJoinTask 两种:RecursiveTask 有返回值 / RecursiveAction 无返回值
☆ ForkJoinPool.commonPool():parallelStream 底层用的全局共享池,IO 密集任务别用(会阻塞其他 stream)
☆ 与 ThreadPoolExecutor 的区别:FJP 适合可拆分递归任务,TPE 适合独立任务
☆ join() 不阻塞:会主动帮忙执行其他任务(窃取或自己队列),等所有子任务完成
⚠️ 易混淆
偷头部还是尾部?自己取尾部、偷别人头部(双端队列两端语义不同)
头部 = 老任务(父任务,颗粒大);尾部 = 新任务(子任务,颗粒小)
commonPool 是全局共享的,IO 阻塞会影响整个应用所有 parallelStream
ForkJoinPool 不是 ThreadPoolExecutor 的子类,是平级实现
🎤 面经
[腾讯]R5 #7127 Java 的线程池,ForkJoinPool 工作窃取怎么保证任务只被一个线程消费?
答:CAS 偷头部任务,偷成功的线程独占执行,原任务节点被替换标记
[Shopee]R1 #790 ForkJoinPool 工作窃取:如何用 ForkJoin 实现百万级订单数据的并行统计?
答:RecursiveTask 拆分订单数组,每段单独计算,join 合并;递归到阈值(如每段 1000 条)停止拆分
[other]R1 #4382 Completablefuture/ForkJoinPool 是啥?
答:ForkJoinPool 是分治+工作窃取的并发框架;CompletableFuture 异步编排默认用 commonPool
[腾讯]R5 #7127 线程池内部有什么数据结构?
答:ThreadPoolExecutor 用 BlockingQueue;ForkJoinPool 每线程双端队列 WorkQueue + 工作窃取
[蚂蚁]R1 #6705 从 ConcurrentHashMap 一路问到锁优化、LongAdder、伪共享、缓存行填充、CAS?
答:工作窃取也涉及伪共享:每个 WorkQueue 数组用 @Contended 缓存行填充避免相邻线程互相 invalidate
threadpool-dynamic
🎯 骨架
★ ThreadPoolExecutor 原生支持运行时调整:setCorePoolSize() / setMaximumPoolSize() / setKeepAliveTime()
★ 配合配置中心(Nacos / Apollo):监听配置变更回调 → 动态修改 → 无需重启
★ 监控指标必须同步:activeCount / queueSize / completedTaskCount / rejectedCount,没监控的动态调没意义
★ 美团开源 DynamicTp:开箱即用,支持告警、动态调参、多种队列、运行时切换拒绝策略
★ 自研动态线程池价值:业务定制(埋点告警/链路 traceId 透传/任务降级),通用方案覆盖不到
☆ 队列大小不能动态改:LinkedBlockingQueue 的 capacity 是 final,要么换实现要么自定义可变队列
☆ 调参后需验证:建议先压测再灰度发布,盲目改可能放大故障
☆ 反模式:调了核心线程数没监控 → 不知道有没有生效,下次出问题更难排查
⚠️ 易混淆
不是所有参数都能动态调(队列容量 capacity 是 final)
setCorePoolSize 减少时不是立即销毁多余线程,而是逐步过期回收
动态线程池 ≠ 弹性线程池,弹性强调"自动伸缩",动态强调"运行时可改"
DynamicTp 是开源方案,不是 JDK 内置
🎤 面经
[蚂蚁]R1 #6642 Spring Cloud 以及外部都有提供动态线程池,为什么还要自己写一套?
答:业务定制:埋点告警/链路传递/任务降级;通用方案覆盖不到细节,自研更灵活
[字节]R1 #4882 线程池是如何创建的,如何动态修改?
答:ThreadPoolExecutor 暴露 setter;配合 Nacos/Apollo 配置中心监听变更回调
[携程]R1 #2942 线程池参数合理,考虑实际场景,code Review 时如何发现代码性能问题的?
答:不能拍脑袋设;动态线程池+监控指标驱动;review 看队列是否有界、拒绝策略是否合理
[蚂蚁]R1 #4092 核心参数配置,相关注意点?
答:业务量变化时核心数要动态调;配置中心监听变更走 setter 接口
[蚂蚁]R1 #6662 高并发情况下如何使用线程池?
答:动态线程池调参 + MQ 削峰 + 监控告警;DynamicTp 等开源方案做参考
S5 并发工具箱与实战
concurrent-hashmap
🎯 骨架
★ JDK 7:三层结构 Segment[]→HashEntry[]→链表,每个 Segment(继承 ReentrantLock)独立加锁,默认 16 段
★ JDK 8:去掉 Segment,Node[]+链表/红黑树;空桶 CAS 写入(无锁),有冲突 synchronized 锁桶头
★ JDK 8 改进:链表 ≥ 8 且数组 ≥ 64 转红黑树(O(n)→O(logn));多线程协同扩容
★ 协同扩容:transferIndex+CAS 领取桶段,ForwardingNode 标记已迁移并转发读到新表,扩容期间读不阻塞
★ size() 弱一致性:用 baseCount + CounterCell 数组(类似 LongAdder)分段累加,并发修改时不精确
★ 复合操作不原子:if (!map.containsKey) put 不安全,必须用 putIfAbsent / computeIfAbsent / merge
☆ 为什么 JDK 8 用 synchronized 不用 ReentrantLock?JDK 6+ 偏向锁/轻量级锁优化后差距小,且 synchronized 不占额外内存(无 AQS 节点)
☆ 为什么放弃 Segment?锁粒度太粗(一段管多桶)、并发度天花板固定(默认 16)、扩容效率低(单线程)
⚠️ 易混淆
JDK 8 不是完全去掉锁,是降到桶级(synchronized 锁单个 Node)
size() 不精确,要精确用 mappingCount() 也不能保证(同一时刻可能在并发修改)
ConcurrentHashMap 不允许 null key 和 null value(HashMap 允许 1 个 null key)
红黑树转换条件是"链表长度≥8 AND 数组长度≥64",只满足前者会先扩容数组而非转树
🎤 面经
[拼多多]R3 #7024 ConcurrentHashMap 怎么实现线程安全的?HashMap 多线程有什么问题?
答:JDK8 CAS+synchronized 锁桶头;HashMap 多线程 put 可能死循环(JDK7 头插)/数据丢失
[百度]R2 #7200 Java8 之后的 ConcurrentHashMap,舍弃分段锁的原因?
答:锁粒度太粗(16 段不够细)+ 维护多个 ReentrantLock 内存开销 + JDK6+ synchronized 已优化
[蚂蚁]R1 #6663 concurrenthashmap 1.8 的改动?
答:数据结构 Segment→Node[]+红黑树;锁机制 ReentrantLock→synchronized;扩容单线程→多线程协同
[小米]R3 #7225 HashTable 和 ConCurrentHashMap 的区别?
答:Hashtable 整张表一把锁,CHM 桶级锁;性能差几个数量级
[B站]R1 #2759 Hashmap、concurrenthashmap、hashtable 各自的特点区别。jdk1.8 都做了哪些改进?
答:CHM 1.8 用 CAS+synchronized 桶级锁 + 红黑树 + 协同扩容
[腾讯]R2 #3149 hashmap、hashtable、concurrenthashmap 实现?
答:HashMap 不安全,Hashtable 全表锁,CHM 桶级锁性能最优
[百度]R2 #7183 ConcurrentHashMap 与 Hashtable 之间的区别?
答:CHM 桶级锁性能远超 Hashtable 全表锁;CHM 1.8 还支持红黑树
[蚂蚁]R1 #6705 从 ConcurrentHashMap 一路问到锁优化、LongAdder、伪共享、缓存行填充、CAS?
答:CHM 空桶 CAS;size 用 LongAdder 分段累加;伪共享靠缓存行填充;底层 cmpxchg 指令
[美团]R1 #3102 concurrent hashmap 如何保证并发安全?
答:JDK8:空桶 CAS、有冲突 synchronized 锁桶头;扩容期间 ForwardingNode 转发读
[蚂蚁]R1 #6861 ConcurrentHashMap?
答:桶级锁 + CAS + 红黑树 + 协同扩容,分段锁的进化版
threadlocal-leak
🎯 骨架
★ 原理:每个 Thread 持有一个 ThreadLocalMap,key = ThreadLocal 弱引用,value = 强引用
★ 泄漏链路:ThreadLocal 对象被 GC → key 变 null(因为弱引用)→ value 还在 → Entry 不可达但 value 占内存
★ 线程池场景最严重:线程不销毁,ThreadLocalMap 一直存在,value 永远不会被回收 + 数据串台风险
★ 解法:用完必须 try-finally remove(),零例外;最佳实践 ThreadLocal 声明为 static final
★ 数据串台 > 内存泄漏:线程池复用时,上一个请求的用户数据被下一个请求读到,是更严重的业务问题
☆ 为什么 key 用弱引用:如果用强引用,外部把 ThreadLocal 引用置 null,Entry 也不会被回收,更糟
☆ InheritableThreadLocal:父线程的值传给子线程(创建时拷贝);线程池场景失效
☆ TransmittableThreadLocal(TTL):阿里方案,解决线程池中父子值传递
⚠️ 易混淆
弱引用是"防御性兜底",不能替代主动 remove
ThreadLocalMap 不是 HashMap,是 AQS 风格的开放寻址(线性探测)
最大问题不是内存泄漏(量级有限),是数据串台(业务正确性)
InheritableThreadLocal ≠ TransmittableThreadLocal,前者只在 new Thread 时拷贝
🎤 面经
[百度]R2 #7182 ThreadLocal 用过吗?不 remove 掉会有什么问题?
答:线程池复用线程时数据串台 + value 永不回收造成内存泄漏;必须 try-finally remove
[美团]R1 #3095 ThreadLocal 的原理?还知道哪些 ThreadLocal 的变种实现?
答:每线程持有 Map,key 是弱引用 ThreadLocal;变种:InheritableThreadLocal(父子传递)、TTL(线程池场景)
[腾讯]R3 #3135 threadlocal 的实现,原理,业务用来做什么?
答:ThreadLocalMap 存 Entry;业务:用户上下文、TraceID、数据库连接、事务隔离
[滴滴]R1 #2981 ThreadLocal 原理?
答:每个线程持有自己的 ThreadLocalMap,key 弱引用,value 强引用,需 remove 防泄漏
[蚂蚁]R1 #6866 ThreadLock 用过没有,说说它的作用?
答:线程隔离的变量副本,常用于用户上下文、TraceID 透传,注意 finally remove
[百度]R2 #7160 ThreadLocal 在 JVM 哪个区?什么时候回收?
答:ThreadLocalMap 在线程对象的实例字段中(堆);线程销毁时 Map 整体回收,否则需主动 remove
[快手]R1 #2734 threadlocal 的 map 为什么没有 hash 冲突;和 hashmap 的区别?
答:ThreadLocalMap 用开放寻址(线性探测),HashMap 用拉链法;冲突很少所以选开放寻址
[蚂蚁]R1 #6849 一个放进 threadlocal 的 hashmap 会放在新生代还是老年代?
答:看对象大小和年龄;ThreadLocalMap 引用链:Thread→ThreadLocalMap→Entry→value
countdownlatch-barrier
🎯 骨架
★ CountDownLatch:一次性倒计数,主线程 await 等子任务全部 countDown 到 0;不可重置
★ CyclicBarrier:N 个线程互相等齐,到齐一起放行;可重置(reset)反复用,有 broken 机制
★ 计数方向相反:CDL 倒数到 0;CB 累加到 N(参与线程都到齐)
★ 等待方向不同:CDL 是"一个线程等多个任务完成";CB 是"多个线程互相等"
★ 底层不同:CDL 基于 AQS 共享模式;CB 基于 ReentrantLock + Condition
★ 场景:CDL 主线程等多个并发查询全部返回;CB 多线程分阶段计算(每阶段等所有线程到齐)
☆ 生产首选 CompletableFuture.allOf() + orTimeout():支持超时+取消+链式组合,比 CDL 强
☆ Semaphore 是另一类(限流):控制同时进入临界区的线程数,常用于限流降级
⚠️ 易混淆
CDL 不可重置(一次性),CB 可 reset(循环用)
CDL 任意线程都能 countDown,CB 必须 N 个线程各调一次 await
CB 的 broken:某个线程超时/中断 → 屏障破损,其他线程也抛 BrokenBarrierException
携程社招提示 "countDownLatch 不太好" 主要是说没超时支持,建议 CompletableFuture
🎤 面经
[携程]R1 #5247 CountDownLatch 和 CyclicBarrier,哪个可以复用,为什么?
答:CB 可复用(reset 重置计数器);CDL 一次性(state 减到 0 不能恢复)
[美团]R4 #3960 Countdownlatch 和 Cyclicbarrier 的区别,分别在什么应用场景下使用?
答:CDL 主线程等子任务(如并发查询汇总);CB N 线程互相等(如多线程分阶段计算/赛马)
[拼多多]R3 #1245 CountDownLatch 和 cyclicBarrier 的区别?
答:CDL 倒数+一次性+主等子;CB 累加+可复用+互等;底层 AQS 共享 vs Lock+Condition
[字节]R1 #4824 线程间同步机制:CountDownLatch CyclicBarrier Semaphore LockSupport?
答:4 个工具类各有侧重:等多个完成/互相等齐/限流/底层 park-unpark
[B站]R1 #2762 juc 中的 countdownlatch,其概念,使用场景?
答:一次性倒计数,常用主线程等并发任务全部完成(如批量数据初始化、并行查询合并)
[携程]R1 #2892 CountDown 等待所有线程减一结束,然后主线程获取 Future 中的结果?
答:经典模式:N 个子任务结果存 List,countDown;主线程 await 后从 List 读结果(生产用 allOf 更优)
[得物]R1 #5798 countdownlatch 的实现原理和底层数据结构?
答:基于 AQS 共享模式,state 表示剩余计数,countDown=state-1,await=tryAcquireShared 等到 0
[阿里]R4 #145 10 个线程模拟赛马,所有马就绪后才能开跑,所有马到达终点后裁判宣布成绩?
答:CB(10) 等所有马就绪 → 起跑 → CDL(10) 等所有马到达终点 → 裁判宣布
[携程]R1 #5257 cyclicbarrier 的实现?
答:ReentrantLock + Condition;count==0 时 signal 所有线程,可 reset 重置 count
[携程]R1 #2942 countDownLantch 为什么不太好?
答:没超时支持,子任务卡死会导致主线程永远等;CompletableFuture.allOf().orTimeout() 更优
concurrent-troubleshoot
🎯 骨架
★ 死锁排查:jstack 找 "Found one Java-level deadlock";arthas thread -b 直接定位元凶
★ CPU 飙高排查三步:
top -Hp 找最高 CPU 的线程 tid
printf '%x\n' 转 16 进制
jstack | grep 找堆栈
★ 内存泄漏:jmap -histo 看对象分布 → jmap -dump 导出堆 → MAT/VisualVM 分析
★ ThreadLocal 泄漏:堆 dump 搜 ThreadLocalMap$Entry,找 key=null 但 value 大的条目
★ 线程池问题:监控 activeCount / queueSize / rejectedCount,队列持续涨说明消费跟不上
☆ arthas 常用命令:thread(线程概览)/ thread -b(找阻塞元凶)/ trace(方法调用耗时)/ watch(观察方法参数返回值)/ dashboard(实时监控)
☆ 数据竞争:开 -XX:+UnlockDiagnosticVMOptions -XX:+DTraceMonitorProbes 看锁竞争
☆ 预防比排查重要:Code Review + 压测 + 监控告警 三件套
⚠️ 易混淆
top -p 看进程 CPU,top -Hp 看进程内线程 CPU,排查 CPU 飙高用 -Hp
jstack 在 STW 时打印,对应用有微小影响;生产慎用频繁
ThreadLocal 泄漏在堆 dump 中表现:大量 ThreadLocalMap$Entry 引用链(Thread→ThreadLocalMap→Entry→value)
arthas 是动态 attach,不需要重启应用;jstack/jmap 也不需要重启
🎤 面经
[阿里]R1 #138 你们遇到过哪些并发的问题,讲一个你印象最深的,是怎么定位并解决的?
答:故事化讲清:现象 → top-Hp 找 CPU 飙高线程 → jstack 看堆栈 → 定位 ThreadLocal 串台 → 加 finally remove + Code Review
[京东]R3 #2200 hashmap 多线程并发情况下有什么问题(jdk1.7 hashmap 成环)?
答:JDK7 头插法扩容时多线程触发链表环;GET 时无限循环 CPU 飙高;用 ConcurrentHashMap 替代
[Shopee]R2 #737 JMM 内存模型 volatile 关键字,i++ 会有并发问题吗?
答:会,i++ 是读-改-写三步非原子;volatile 只保证可见性不保证原子;用 AtomicInteger
[other]R1 #4355 threadlocal 能解决并发的问题吗?threadlocal 会有内存泄露的问题吗?
答:解决线程内变量隔离不是并发互斥;会内存泄漏(key 弱引用+value 强引用),更严重的是数据串台
[快手]R1 #2677 对线程安全有什么理解;内存泄露;死锁?
答:三大类排查:CPU 飙高(top-Hp+jstack)/内存泄漏(jmap+MAT)/死锁(jstack+arthas thread -b)
[京东]R2 #5962 Java 中怎么线程同步?并发问题?
答:同步 synchronized/Lock/CAS;并发问题:可见性、原子性、有序性、死锁、活锁、饥饿
[拼多多]R3 #6948 事务的并发问题?
答:脏读/不可重复读/幻读;隔离级别 RU/RC/RR/Serializable;MySQL 默认 RR + 间隙锁防幻读
[other]R1 #4488 如何避免并发操作连多?redis 记录连麦人数?为啥用 redis 可以解决并发问题?单线程?
答:Redis 单线程命令原子;INCR/DECR 计数避免 check-then-set 竞态
[other]R1 #4514 慢 SQL 排查(侧面看并发)?
答:EXPLAIN 看执行计划 type/key/extra;index condition push 索引下推;filesort 排序
OS
S* 通用
process-vs-thread
🎯 骨架
进程是资源分配的最小单位(独立地址空间),线程是 CPU 调度的最小单位(共享进程地址空间)
切换开销:进程 > 线程 > 协程;进程切换额外要换页表 + 刷 TLB
切换 90% 开销在缓存冷启动(cache miss),不在寄存器保存恢复
Linux 中线程和进程共用 task_struct,区别在于是否共享 mm_struct(内存描述符)
协程是用户态调度,OS 不感知;纳秒级切换,几 KB 栈
是否涉及内核:进程/线程切换由内核完成,协程切换全程用户态
高并发不是堆线程数,频繁切换缓存反复污染反而更慢(Netty EventLoop 单核绑定哲学的来源)
⚠️ 易混淆
"是否涉及内核"≠ 是否保存寄存器(三者都要保存寄存器),指调度决策本身是否进内核态
Java 的 Thread 在 Linux 上对应 NPTL 的内核线程,1:1 映射,不是用户态线程
虚拟线程不是协程:底层仍依赖 epoll 系统调用,只是把挂起/恢复放到用户态做
🎤 面经
[滴滴]R1 #2976 进程和线程的区别
答:进程是资源分配单位(独立地址空间),线程是调度单位(共享进程内存),切线程不切页表
[腾讯]R1 #5044 进程和线程区别
答:进程独立地址空间隔离强,线程共享进程内存通信快但易出并发问题
[阿里]R1 #3 什么是进程?和线程的区别?
答:进程是程序的执行实例 + 资源容器;线程是进程内调度单位,共享代码段和堆
[蚂蚁]R1 #6698 虚拟内存分页了解不?进程和线程区别?
答:进程切换换页表刷 TLB 开销大,线程切换只换寄存器和内核栈,mm_struct 不变
[滴滴]R1 #2516 进程和线程的区别是什么?进程之间怎么通信的?线程之间呢?
答:区别同上;进程间靠 IPC(管道/共享内存/Socket),线程间共享堆 + 锁同步
[字节]R1 #373 进程和线程的区别。线程之前的通信方式,进程之间的通信方式
答:线程共享堆栈直接读写,加锁即可;进程要走 IPC,速度共享内存最快 Socket 最慢
[小红书]R2 #4127 除了进程,线程还有哪些?
答:还有协程(用户态调度,纳秒级切换),Java 21 的虚拟线程基于 carrier thread 实现
[阿里]R4 #169 进程和线程的区别
答:资源 vs 调度;切换开销 90% 在缓存冷启动,不是寄存器
deadlock
🎯 骨架
四个必要条件:互斥 / 持有并等待 / 不可剥夺 / 循环等待,缺一不可
破坏循环等待最常用:统一资源申请顺序(如转账按账户 ID 从小到大加锁)
破坏持有并等待:一次性申请所有资源(TCC Try 阶段)或 tryLock 超时放弃
破坏不可剥夺:MySQL 死锁检测主动回滚代价小的事务、Redis 锁的 TTL 兜底
MySQL 经典死锁:反向加锁(事务 A 锁 id=1→id=2,事务 B 锁 id=2→id=1)
RC 隔离级别能消除 Gap Lock 死锁,但仍存在行锁死锁和唯一键冲突死锁
排查:SHOW ENGINE INNODB STATUS\G 看 LATEST DETECTED DEADLOCK 段
⚠️ 易混淆
死锁检测 ≠ 死锁预防:检测是事后回滚,预防是事前破坏条件
InnoDB 用 wait-for graph(等待图)做死锁检测,不是超时检测
Java 的 ReentrantLock + tryLock(timeout) 是破坏"持有并等待",不是破坏"不可剥夺"
🎤 面经
[腾讯]R1 #3129 死锁是怎么产生的
答:互斥+持有等待+不可剥夺+循环等待四条件同时满足时产生
[滴滴]R1 #2978 死锁条件,解决方式
答:四条件;解决破坏任一条件,最常用统一加锁顺序破环
[蚂蚁]R1 #6903 加锁有几种方式,死锁的条件,设计一个场景会不会死锁怎么避免
答:互斥锁/读写锁/自旋锁;A 锁 X 等 Y、B 锁 Y 等 X 即死锁;统一顺序避免
[蚂蚁]R4 #4149 死锁问题的分析与具体解决办法(非八股,具体排查思路与实践过程)
答:SHOW ENGINE INNODB STATUS 看锁持有/等待,业务上统一加锁顺序 + DeadlockException 重试
[蚂蚁]R4 #4180 什么时候多线程会发生死锁,写一个例子吧
答:两线程交叉获取 synchronized(A){synchronized(B)}和 synchronized(B){synchronized(A)} 即死锁
[网易]R3 #3206 死锁怎么预防。怎么检测死锁
答:预防:破坏四条件;检测:等待图算法 + jstack 看 BLOCKED 线程持有/等待锁链
[蚂蚁]R4 #4150 间隙锁死锁原因与排查思路?如何预防?
答:RR 下间隙锁互相阻塞产生死锁;改 RC 或统一 SQL 执行顺序避免
[网易]R1 #3226 场景题:手写死锁(Synchronized 和 ReentrantLock)
答:两线程反向获取两把锁即可,ReentrantLock 用 tryLock 超时可避免
zombie-orphan-process
🎯 骨架
僵尸进程:子进程已退出,父进程没 wait() 读取退出状态,PCB 留在内核占 PID
孤儿进程:父进程先退出,子进程被 init(PID=1)收养,init 定期 wait() 回收,无害
僵尸进程的危害:耗尽 PID(Linux 默认约 32768)→ 无法创建新进程
守护进程的标准做法 = 故意制造孤儿进程(fork 后父退子留,被 init 接管)
Java 场景:ProcessBuilder 启动子进程必须 waitFor() 或 destroy(),否则变僵尸
排查:ps aux | grep Z 看状态为 Z 的进程;找到父进程 kill 让 init 接管
fork() 的 COW(写时复制):父子初始共享物理页,写时才拷贝
⚠️ 易混淆
僵尸进程不能用 kill -9 直接杀(已经死了),要杀父进程让 init 收养再回收
孤儿进程不是问题,被 init 收养正常回收;僵尸才是问题
waitpid() 是显式回收;signal(SIGCHLD, SIG_IGN) 是让内核自动回收子进程
🎤 面经
[字节]R1 #3814 fork 函数,父子进程的区别,孤儿进程,僵尸进程会有什么问题
答:僵尸占 PID 可能耗尽进程表;孤儿被 init 接管无害
[字节]R4 #713 进程调度;僵尸进程和孤儿进程的区别
答:僵尸是子死父没 wait;孤儿是父死子被 init 收养
[字节]R2 #490 僵尸进程和孤儿进程;孤儿进程多了会有什么影响
答:孤儿被 init 回收无害;不像僵尸会占 PID
[京东]R1 #5505 僵尸进程和孤儿进程
答:僵尸:PCB 滞留,需父 wait;孤儿:被 init 收养自动 wait
[字节]R3 #3553 操作系统的进程通信方式,僵尸进程和孤儿进程是什么,如何避免僵尸进程
答:父进程显式 wait() 或忽略 SIGCHLD 让内核自动回收;ProcessBuilder 必须 waitFor()
[腾讯]R1 #3130 代码中遇到进程阻塞,进程僵死,内存泄漏等情况怎么排查
答:ps 看状态、jstack 看线程栈、jmap dump 分析,僵尸进程找父进程 kill
[网易]R1 #3253 Linux 的 fork 指令对数据的拷贝是马上就拷贝的吗?
答:不是,COW 写时复制,初始共享物理页只复制页表,写时才真正分裂
ipc-overview
🎯 骨架
六种方式:管道 / 消息队列 / 共享内存 / 信号量 / 信号 / Socket
速度排名:共享内存 > 信号量 > 管道 ≈ 消息队列 > 信号 > Socket
共享内存最快:多进程虚拟地址映射同一块物理内存,零拷贝
共享内存的代价:要配合信号量/互斥锁做同步,否则数据竞争
Socket 最通用:跨机器通信走 TCP/UDP;本机用 Unix Domain Socket(UDS)跳过协议栈
信号 vs 信号量:信号是 OS→进程的异步通知(编号),信号量是同步原语(计数器)
Java 中的对应:MappedByteBuffer(共享内存)/ Semaphore(信号量)/ PipedStream(管道)/ ShutdownHook(信号)
⚠️ 易混淆
消息队列 vs 管道:消息队列有消息边界且支持类型选择接收,管道是字节流
Redis 的"内存数据库"≠ 共享内存:Redis 是单进程堆内存,不是 IPC 共享内存
Unix Domain Socket 用 Socket API 但不走网络协议栈,速度接近共享内存
🎤 面经
[滴滴]R1 #2977 进程间通信方式 IPC
答:六种:管道/消息队列/共享内存/信号量/信号/Socket,速度共享内存最快
[拼多多]R3 #6951 对于线程和进程的理解。进程间通信方式
答:进程间通信用 IPC 六种方式,共享内存零拷贝最快但要加锁同步
[网易]R1 #3174 进程间通信的方式有哪些
答:管道(流式无边界)/ 消息队列(有边界可选类型)/ 共享内存(最快)/ 信号量(同步)/ 信号 / Socket(最通用)
[阿里]R4 #6561 进程通信方式
答:管道、消息队列、共享内存、信号量、信号、Socket,按场景选
[百度]R2 #5638 进程间通信 IPC 的方式有哪些,然后问哪个是最快的
答:共享内存最快,零拷贝直接读写同一块物理内存,不经过内核
[字节]R4 #351 进程间的通讯方式
答:同上六种;本机优选共享内存 + 信号量;跨机用 Socket
[网易]R1 #1639 进程和线程;进程间通信;介绍信号量
答:信号量是计数器+等待队列,P 操作减一为负则阻塞,V 操作加一唤醒
[腾讯]R2 #3150 Redis 内存数据库的内存指的是共享内存么
答:不是,Redis 是单进程的进程堆内存,通过 malloc 分配,不是 IPC 共享内存
shared-memory
🎯 骨架
多进程虚拟地址映射到同一块物理内存,读写不经过内核,零拷贝
是 IPC 中速度最快的方式:直接读写物理内存,省了内核态↔用户态的两次拷贝
必须配合信号量/互斥锁做同步,否则并发读写数据竞争
mmap 是共享内存的常见实现:FileChannel.map → MappedByteBuffer
mmap 第一次访问触发 Page Fault → 从磁盘加载到 Page Cache → 映射到虚拟地址
应用:Chrome 多进程渲染共享显存、Kafka Producer→Broker 写入用 mmap
Redis 不是共享内存:Redis 是单进程,内存是进程堆内存,不是 IPC 共享
⚠️ 易混淆
共享内存 ≠ Page Cache:所有文件 IO 都经 Page Cache,共享内存是进程间共享的物理页
mmap vs read:read 多一次内核缓冲区到用户缓冲区的拷贝,mmap 直接映射 Page Cache
Java 的 MappedByteBuffer 底层是 mmap,但只能在同一进程内多线程共享,跨进程要文件 mmap
🎤 面经
[腾讯]R2 #3150 Redis 内存数据库的内存指的是共享内存么;Redis 的持久化方式
答:不是,Redis 是单进程堆内存(malloc)不是 IPC 共享内存;持久化用 RDB+AOF
[腾讯]R4 #3531 Redis 内存数据库的内存指的是共享内存么
答:不是,Redis 进程独占内存,"内存数据库"指数据放堆内存而非 IPC 共享内存
[网易]R1 #5569 进程间的通信。共享内存在 JAVA 中是怎么体现的
答:MappedByteBuffer / FileChannel.map,底层是 mmap,可跨进程共享文件映射
[百度]R2 #5638 进程间通信 IPC 的方式有哪些,然后问哪个是最快的
答:共享内存最快,零拷贝直接读写同一块物理内存
[滴滴]R1 #2523 Kafka 的 log,index,稀疏索引。零拷贝,mmap,sendfile、DMA gather
答:Kafka 写用 mmap(共享内存)写入 Page Cache,读用 sendfile 走零拷贝
[滴滴]R1 #2977 进程间通信方式 IPC
答:共享内存是六种 IPC 之一,最快但要配合信号量同步
pipe-socket
🎯 骨架
管道:内核 64KB 缓冲区,单向流动,字节流无边界(cat | grep | wc)
管道是流式的,三个进程同时启动并行运行,像水管一段段流过
Socket:通过网络协议栈通信,支持本机和跨机器,最通用但最慢
Unix Domain Socket(UDS):本机优化,跳过网络层在内核缓冲区直接传
TCP Socket 状态:LISTEN(服务端等连接)/ ESTABLISHED / CLOSE_WAIT(被动关闭等 close)
CLOSE_WAIT 堆积说明应用没正确 close();TIME_WAIT 是主动关闭方等 2MSL
应用:Docker daemon 用 UDS、Nginx 反代后端用 UDS、浏览器访问网站用 TCP Socket
⚠️ 易混淆
管道(pipe)vs 命名管道(FIFO):pipe 只能父子进程间用,FIFO 通过文件名跨任意进程
Socket vs 消息队列:Socket 是流式连接,消息队列有消息边界
WebSocket ≠ Socket:WebSocket 是基于 TCP 的应用层协议,Socket 是 OS 接口
🎤 面经
[字节]R1 #3814 fork 函数...进程间怎么同步,通信,消息队列,管道怎么实现的
答:管道是内核 64KB 环形缓冲区,半双工字节流,写满阻塞写端读空阻塞读端
[网易]R1 #3282 用代码实现 cat 1.log | grep a | sort | uniq -c | sort -rn 的功能
答:用 fork + pipe + dup2 把前一进程 stdout 接到下一进程 stdin,多次串联
[蚂蚁]R1 #6843 TCP 的 LISTEN 状态是什么?TCP 的 CLOSE_WAIT 状态是什么?
答:LISTEN 服务端等连接;CLOSE_WAIT 被动关闭方收到 FIN 等本端 close()
[百度]R2 #5637 对套接字编程的详细过程进行描述,讲解 tcp 三次握手四次挥手
答:socket→bind→listen→accept;客户端 socket→connect;三次握手在 connect 内完成
[字节]R3 #3541 socket 网络编程,说一下 TCP 的三次握手和四次挥手
答:SYN/SYN-ACK/ACK 建立;FIN/ACK/FIN/ACK 关闭,主动关闭方进入 TIME_WAIT
[小米]R1 #3638 tcp 握手挥手过程是怎样的?socket 的状态变化
答:服务端 LISTEN→SYN_RCVD→ESTABLISHED;客户端 SYN_SENT→ESTABLISHED
[百度]R2 #5637 分析服务器端 CLOSE_WAIT 进程太多的原因是什么
答:应用没正确 close() socket,文件描述符泄漏,导致 CLOSE_WAIT 堆积
[滴滴]R3 #2993 长轮询、短轮询、SSE、WebSocket 优缺点
答:WebSocket 全双工长连接,建立一次复用;轮询每次重新建 TCP 浪费连接
virtual-memory
🎯 骨架
虚拟内存让每个进程都以为自己独占整片地址空间,OS + MMU 硬件做地址翻译
虚拟地址 → 页表翻译 → 物理地址;页表由 OS 维护,每个进程一份
价值:内存隔离 / 按需加载 / 比物理内存更大的地址空间 / 简化编译器
物理内存(RAM)有两类:进程私有页(堆栈代码段)+ Page Cache(文件缓存)
Page Cache 不是 mmap 独有,所有文件 IO(read/write/mmap)都经过它
申请 100B 内存,OS 不会马上分配物理页,写时才触发缺页中断真正分配(Lazy Allocation)
页面置换算法:LRU(最近最少使用)/ LFU(最不经常使用);Linux 用近似 LRU 的二级链表
⚠️ 易混淆
虚拟内存 ≠ 交换分区(Swap):虚拟内存是地址空间抽象,Swap 是物理内存不够时换出到磁盘
写时复制(COW)发生在 fork:父子初始共享物理页,写时才真分裂
TLB 是页表的硬件缓存,进程切换会刷 TLB,所以进程切换比线程切换贵
🎤 面经
[腾讯]R1 #5012 虚拟内存和物理内存
答:虚拟内存是进程视角的地址空间抽象,物理内存是 RAM;MMU 通过页表把虚拟地址翻译成物理地址
[腾讯]R5 #7081 虚拟内存和物理内存
答:同上;价值在隔离 + 按需加载 + 让程序看到比 RAM 更大的地址空间
[Shopee]R2 #990 虚拟内存的作用?原理?
答:作用:隔离/按需/扩大寻址;原理:页表翻译 + MMU + 缺页中断按需加载
[阿里]R1 #6 什么是虚拟内存?除了内存扩展,虚拟内存还有什么作用
答:进程地址空间抽象;除了扩展还有进程隔离、共享库共享、写时复制、按需加载
[Shopee]R1 #6421 程序申请 100 字节的内存,操作系统是马上拿出 100 字节的内存吗?
答:不会,只分配虚拟地址,物理页要等真正写入时缺页中断才分配(Lazy Allocation)
[字节]R1 #457 虚拟内存,虚拟地址和物理地址怎么转换,内存分段,内存分页,优缺点
答:分段按逻辑划分易碎片,分页固定大小(4KB)减少碎片但要页表,现代 OS 都用分页
[腾讯]R1 #1655 虚拟内存和物理内存;页面置换算法,LRU 底层是啥
答:LRU 用双向链表 + 哈希表,访问移到头部,淘汰尾部;Linux 用 active/inactive 二级链表近似
[网易]R1 #1626 介绍一下虚拟内存;内存和磁盘之间怎么进行页面的交换
答:页面置换;缺页中断 → 选择牺牲页 → 脏页写回 swap → 加载新页 → 更新页表
page-table
🎯 骨架
页表:虚拟页号 → 物理页框号的映射,由 OS 维护,MMU 硬件查表
多级页表(如 x86-64 四级):解决单级页表占用大问题,按需分配
TLB(Translation Lookaside Buffer):页表的硬件缓存,命中则不用查内存中的页表
缺页中断三种:硬缺页(页在磁盘)/ 软缺页(页在内存但未映射)/ COW 缺页
缺页处理:触发中断 → OS 选择物理页(必要时换出)→ 加载到 RAM → 更新页表 → 重新执行指令
进程切换刷 TLB(CR3 寄存器变化),后续访问全 cache miss 是切换贵的根因
内存抖动(Thrashing):缺页太频繁导致大部分时间在换页而不是执行
⚠️ 易混淆
缺页中断不是错误,是按需加载机制;段错误(SIGSEGV)才是越权访问错误
页表项只标记"在不在内存"和"映射到哪个物理页",不存数据本身
TLB 命中率对性能极重要;大页(HugePage)通过减少 TLB 项数提升命中率
🎤 面经
[网易]R1 #3252 缺页中断,分页地址转换,内存抖动
答:缺页:页不在物理内存触发中断;地址转换:虚拟页号→页表→物理页框;抖动:缺页太频繁
[Shopee]R1 #6422 程序进行中发现页块不在内存中,详细说明之后会发生什么
答:触发缺页中断 → 内核找空闲物理页(不够则换出脏页)→ 从磁盘加载 → 更新页表 → 重启指令
[蚂蚁]R1 #6698 虚拟内存分页了解不?进程和线程区别?
答:分页固定 4KB 大小,靠页表 + MMU 硬件翻译,TLB 缓存最近访问的页表项
[Shopee]R1 #6421 程序申请 100 字节的内存,操作系统是马上拿出 100 字节的内存吗?
答:不是,只给虚拟地址不给物理页,写时缺页中断才真正分配
[字节]R1 #457 虚拟内存,虚拟地址和物理地址怎么转换,内存分段,内存分页
答:分页:虚拟地址 = 页号 + 页内偏移,页号查页表得物理页框号,拼接物理页内偏移
[腾讯]R1 #1655 虚拟内存和物理内存;页面置换算法,LRU 底层是啥
答:缺页时换页,LRU 用双向链表 + 哈希维护访问顺序,淘汰最久未用
[网易]R1 #1626 介绍一下虚拟内存;内存和磁盘之间怎么进行页面的交换
答:缺页中断驱动;脏页 dirty bit 标记,换出时只有脏页要写回 swap
zero-copy-os
🎯 骨架
传统 read+write:4 次上下文切换 + 4 次拷贝(磁盘→内核缓冲→用户缓冲→Socket 缓冲→网卡)
mmap+write:把内核 Page Cache 直接映射到用户空间,省一次内核↔用户拷贝(变 3 次拷贝)
sendfile:从内核 Page Cache 直接到 Socket 缓冲,全程不进用户态(2 次切换 + 3 次拷贝)
sendfile + DMA gather:Socket 缓冲只存元数据,数据由网卡 DMA 直接读 Page Cache(2 次拷贝)
splice:两个文件描述符间通过内核 pipe 传输,不拷贝到用户态
Kafka 经典组合:Producer→Broker 用 mmap 写、Broker→Consumer 用 sendfile 读
Java 对应 API:FileChannel.transferTo() → 底层 sendfile;FileChannel.map() → 底层 mmap
⚠️ 易混淆
"零拷贝"不是真零拷贝,而是省掉 CPU 参与的内核↔用户拷贝;DMA 拷贝仍在
mmap 的"零拷贝"指 read 端少一次拷贝,写入时 OS 仍要把 Page Cache 刷回磁盘
Netty 的 CompositeByteBuf 也叫零拷贝,但是逻辑层面合并而非 OS 层零拷贝
🎤 面经
[字节]R1 #374 零拷贝的概念是什么意思
答:数据传输不经过用户空间,省掉内核↔用户的 CPU 拷贝;典型 sendfile 和 mmap
[Shopee]R3 #890 zero copy 怎么实现的?
答:mmap 映射 Page Cache 到用户空间;sendfile 从 Page Cache 直接到 Socket 缓冲;Java 用 transferTo
[滴滴]R1 #2523 Kafka 的 log,index,稀疏索引。零拷贝,mmap,sendfile、DMA gather
答:Kafka 写用 mmap 顺序追加,读用 sendfile + DMA gather 网卡直接读 Page Cache
[京东]R1 #5499 零拷贝是什么?用来解决什么问题?有哪些应用场景?实现方式有哪些?
答:解决传统 IO 多次拷贝 CPU 浪费;用于大文件传输、消息队列;mmap/sendfile/splice
[百度]R2 #6040 NIO 的原理,从操作系统说(想问 fd 系统);零拷贝
答:NIO 基于 epoll + 非阻塞 fd;零拷贝靠 sendfile 减少拷贝次数
[拼多多]R4 #1098 网络编程 nio 和 netty 相关,netty 的线程模型,零拷贝实现
答:Netty 零拷贝:CompositeByteBuf 合并、FileRegion 用 sendfile、堆外内存避免 JVM↔OS 拷贝
[京东]R2 #3401 kafka 了解么?...零拷贝
答:Kafka 单机几十万 QPS 关键就是顺序写 + Page Cache + sendfile 零拷贝
linux-troubleshoot
🎯 骨架
CPU 高:top → top -Hp 找高 CPU 线程 → printf "%x" tid → jstack 找十六进制 nid
CPU 低响应慢:可能是 IO wait 或锁竞争,看 top 的 wa 值、iostat、jstack 找 BLOCKED
内存高:free -h 看物理内存、jmap -heap 看 JVM 堆,OOM 用 jmap dump + MAT 分析
磁盘 IO:iostat -x 1 看 %util / await,找慢盘;iotop 看哪个进程
网络:netstat -anp | grep ESTABLISHED 看连接、ss -s 统计、tcpdump 抓包
看日志:tail -f / less +F;grep -A -B 看上下文;awk 统计
通用思路:USE 方法(Utilization 利用率 / Saturation 饱和度 / Errors 错误)
⚠️ 易混淆
top 的 load average 是平均运行+不可中断队列长度,不是 CPU 使用率
iowait 高不一定磁盘问题,也可能是网络 IO 慢
jstack 的 nid 是十六进制,要把 top -Hp 的十进制 tid 转成十六进制对照
🎤 面经
[蚂蚁]R1 #6898 Linux 上面用过什么调优工具,CPU 占用资源很高有可能是什么问题
答:top 看进程,top -Hp 找线程;CPU 高常见死循环、频繁 GC、锁竞争自旋
[蚂蚁]R1 #6898 CPU 低但响应慢可能是什么问题?
答:IO wait 高(磁盘/网络)、锁阻塞、外部依赖慢、GC stop-the-world
[腾讯]R1 #2195 看磁盘 IO 吞吐用什么命令;看网卡流量用什么命令;mpstat iowait 高怎么判断
答:磁盘 iostat;网卡 sar -n DEV / iftop;mpstat 看每核 iowait/user 分布定位瓶颈核
[字节]R2 #468 知道什么 Linux 性能分析相关的命令。top 那几个指标什么意思
答:top 看 us/sy/wa/id;us 用户态 CPU、sy 内核态、wa IO 等待、id 空闲
[网易]R1 #1612 Linux 上怎么定位问题,用什么命令
答:top/free/iostat/netstat/jstack/jmap 组合,先定位资源(CPU/内存/IO/网络)再深入
[拼多多]R1 #872 线上排查 OOM 问题怎么做?查过 Linux 下内存高的进程吗?
答:top 看 RES、jmap -dump:format=b,file=heap.hprof + MAT 分析支配树
[腾讯]R1 #5052 看日志后五行(意指日志查看与分析)
答:tail -n 5 file.log;实时跟踪 tail -f;找 ERROR 用 grep -A 10 -B 5 ERROR
[Shopee]R1 #6430 Linux 系统中跑了一个死循环进程,操作系统怎么发现,或者会出现什么结果
答:top 看 CPU 100%;CFS 调度仍能切换其他线程,但单核被占其他线程响应变慢
docker-basics
🎯 骨架
Docker = namespace(隔离)+ cgroup(资源限制)+ 镜像分层(共享省盘)
和虚拟机区别:VM 虚拟硬件有独立内核,Docker 共享宿主内核 OS 级隔离,秒级启动
namespace 隔离:PID / Network / Mount / UTS / IPC / User 六类
cgroup 限资源:--cpus 限 CPU、--memory 限内存,超内存触发 OOM Kill
镜像分层:Dockerfile 每条指令一层,相同层只存一份;容器 = 只读层 + 可写层
写时复制(CoW):读直接读镜像层,写才从镜像层复制到可写层再改
部署流程:Dockerfile → docker build → docker push → K8s Deployment 滚动更新
⚠️ 易混淆
容器不是轻量级 VM:没有独立内核,内核漏洞可能逃逸
镜像(image)= 只读模板;容器(container)= 镜像运行实例 + 可写层
Layer 是只读的,所有相同 layer 在一个节点上只存一份(写时复制省盘)
🎤 面经
[腾讯]R1 #2052 Docker 和虚拟机的区别;Docker 写时复制了解吗
答:VM 虚拟硬件 GB 级开销分钟启动;Docker 共享内核 MB 级 namespace+cgroup 秒级启动;写时复制让镜像层只读容器写时复制到可写层
[字节]R2 #508 docker 的 image、layer、container 分别讲下?Linux 中 namespace 是怎么实现的
答:image 只读模板;layer 镜像分层每条指令一层;container 镜像实例+可写层;namespace 通过 clone() 系统调用 flags 创建(PID/Net/Mount/UTS/IPC/User)
[百度]R1 #4016 docker 和 kubenetes 的原理和了解程度,docker 的 cgroup 了解么
答:cgroup 是 Linux 内核子系统,控制 CPU/memory/io 等资源使用上限;namespace 隔离视图,cgroup 限资源
[百度]R2 #5666 谈谈你对 docker 的理解(追问:Docker 底层是如何做资源隔离的?)
答:namespace 做隔离(看不到别人)+ cgroup 做限制(不能用太多)+ UnionFS 做镜像分层
[B站]R1 #5840 docker 部署的流程
答:写 Dockerfile → docker build → push 到镜像仓库 → K8s 拉镜像 → 滚动更新(启动新 Pod 健康检查通过再下旧)
[B站]R1 #5847 docker 容器是什么?docker 容器核心组件
答:容器是镜像实例 + 可写层;核心组件 image / container / layer / volume / network
[字节]R1 #617 解释容器化(如 Docker/K8s)在数据产品部署中的价值
答:环境一致性、秒级启动支持弹性、cgroup 资源隔离、镜像标准化交付、高密度部署
NETWORK
S1 网络分层基础
osi-tcp-ip-model
🎯 骨架
OSI 七层(理论):物理 / 数据链路 / 网络 / 传输 / 会话 / 表示 / 应用
TCP/IP 四层(工程):网络接口 / 网际(IP)/ 传输(TCP/UDP)/ 应用(HTTP/DNS)
五层模型 = OSI 简化版:物理 / 数据链路 / 网络 / 传输 / 应用(合并会话+表示+应用)
协议归属:HTTP/DNS/SMTP→应用层,TCP/UDP→传输层,IP/ICMP→网络层,ARP→数据链路层
设备归属:交换机→数据链路层(MAC 寻址),路由器→网络层(IP 寻址)
数据封装方向:应用层往下逐层加头部(段→包→帧→比特流)
记忆口诀:物链网传会表应(自下而上)
⚠️ 易混淆
ARP 是数据链路层协议(解析 IP→MAC),不是网络层
HTTPS 不是独立一层,是应用层 HTTP + 表示层 TLS 的组合
TCP/IP 四层把会话/表示/应用合并到应用层(不是没有)
🎤 面经
[滴滴]R1 #2973 OSI 七层模型与 TCP/IP 五层模型
答:OSI 七层理论分层;TCP/IP 五层工程分层把会话表示应用合并为应用层
[字节]R1 #4834 五层协议
答:物理 / 数据链路 / 网络 / 传输 / 应用,常考各层代表协议和设备
[携程]R1 #5265 网络七层模型和四层模型的区别
答:七层 OSI 含会话表示,四层 TCP/IP 把会话表示应用合并为应用层
[Shopee]R1 #1193 讲讲 OSI 网络模型和 TCP-IP 模型
答:OSI 是理论参考七层;TCP/IP 是工程实现四层,HTTP/TCP/IP 各对应一层
[小红书]R4 #5353 OSI 七层分别是什么?TCP 在哪层,HTTP 协议在哪一层?
答:物链网传会表应;TCP 传输层,HTTP 应用层
[蚂蚁]R1 #3464 七层/五层架构,路由器是在哪层,TCP HTTP 等各种协议是哪层
答:路由器网络层做 IP 寻址;TCP 传输层;HTTP 应用层
[网易]R1 #5566 TCP 在网络中的第几层?TCP 长连接的应用?HTTP 和 TCP 什么关系
答:TCP 传输层;HTTP 是应用层协议,跑在 TCP 之上
url-to-page-flow
⚠️ 易混淆
DNS是递归查询,客户端只问本地DNS一次,不是自己一级一级问
浏览器渲染(DOM/CSSOM)是前端考点,后端面试一句带过
hosts文件优先级高于DNS
TLS握手在TCP握手之后
🎤 面经
[网易/快手/小米][R1] 在浏览器输入URL后经历哪些过程?
答:DNS解析→TCP握手→HTTP请求/响应,HTTP/1.1 Keep-Alive复用连接
[阿里][R1] 从浏览器输入URL到获取数据的网络传输全过程?
答:同上,加:TLS 1.3优化到1-RTT,HTTP/2多路复用解决队头阻塞
[滴滴][R1] URL整个解析过程,涉及DHCP/ARP/DNS/TCP等协议?
答:DHCP拿IP → ARP拿网关MAC → DNS解析 → TCP握手 → HTTP,路由器逐跳转发靠OSPF/BGP
[京东][R2] 如何让某个域名不访问特定IP?
答:hosts文件改指向(优先级最高);DNS服务器改A记录;防火墙DROP目标IP;HTTPS证书是最后防线
[B站][R1] 越详细越好?
答:加缓存层级:强缓存(max-age不发请求)+ 协商缓存(ETag/304省body)+ CDN;加HTTP/3 QUIC
[通用] 为什么TCP是三次握手不是两次?
答:两次只能证明客户端能发+服务端能收发,服务端无法确认客户端能收到自己的包,第三次ACK补全这个确认
S2 可靠传输的艺术
tcp-three-way-handshake
🎯 骨架
三步:客户端 SYN(seq=x) → 服务端 SYN+ACK(seq=y, ack=x+1) → 客户端 ACK(ack=y+1)
状态:CLOSED→SYN_SENT→ESTABLISHED;服务端 LISTEN→SYN_RCVD→ESTABLISHED
三个原因:① 验证双方收发能力 ② 防历史失效 SYN 浪费资源 ③ 双向交换初始序列号 ISN
两次不行:服务端无法确认客户端的接收能力;旧 SYN 误连接耗资源
第三次 ACK 丢失:服务端重传 SYN+ACK 5 次(1/2/4/8/16s);首个数据包带 ACK 自愈
SYN Flood:大量发 SYN 不发 ACK,半连接队列耗尽
防御 SYN Flood:SYN Cookie(不分配资源,把信息编码进序列号)
⚠️ 易混淆
ISN 不是固定从 0 开始,是基于时钟随机生成(防猜测)
第三次 ACK 可携带数据(被称为 piggyback)
半连接队列(SYN_RCVD)和全连接队列(ESTABLISHED)是两个不同队列
🎤 面经
[百度]R2 #7166 TCP 的三次握手过程简述
答:SYN→SYN+ACK→ACK;交换 ISN,验证双方收发能力
[滴滴]R1 #2975 TCP 三次握手过程以及每次握手后的状态改变,为什么三次?两次不行?
答:两次只能验证单向;三次让服务端也能确认客户端能收
[蚂蚁]R1 #6827 HTTP 请求过程,DNS 解析的过程。三次握手和四次握手
答:DNS→TCP 三握→(TLS 握手)→HTTP 收发→四挥
[Shopee]R1 #2779 TCP 的握手与挥手
答:握手 3 次同步 ISN;挥手 4 次因为全双工要双向关闭
[百度]R2 #2832 TCP 和 UDP 的区别,TCP 三次握手 说一下四次挥手
答:握手三步同步序列号;挥手四步分别关闭两个方向
[字节]R1 #4739 三次握手与四次挥手能不能改,为什么
答:三次握手是最少够用次数;挥手四次是因为 ACK 和 FIN 中间隔 CLOSE_WAIT
[阿里]R4 #6559 TCP 三次握手
答:客户端 SYN,服务端 SYN+ACK,客户端 ACK,进 ESTABLISHED
tcp-four-way-wave
🎯 骨架
四步:客 FIN(seq=u) → 服 ACK(ack=u+1) → 服 FIN(seq=v) → 客 ACK(ack=v+1)
状态:客 ESTABLISHED→FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT→CLOSED;服 ESTABLISHED→CLOSE_WAIT→LAST_ACK→CLOSED
为什么四次:TCP 全双工,两个方向独立关闭;服务端 ACK 和 FIN 之间可能还要发剩余数据
TIME_WAIT 在主动关闭方,持续 2MSL(约 60s)
TIME_WAIT 两个作用:① 让最后 ACK 有时间送达,避免对方重传 FIN ② 让旧报文在网络中消亡,防止干扰新连接
CLOSE_WAIT 过多 = 服务端代码忘了 close(),应用层 bug
TIME_WAIT 过多 = 主动关闭方太多(如短连接服务),调小 tcp_fin_timeout / 开启 tcp_tw_reuse
⚠️ 易混淆
TIME_WAIT 出现在主动关闭方,CLOSE_WAIT 出现在被动关闭方
三次挥手是可能的:服务端没有数据要发时把 ACK 和 FIN 合并(延迟确认机制)
MSL(Maximum Segment Lifetime)默认 30s,2MSL = 60s
第四次 ACK 丢失:服务端重传 FIN,客户端在 TIME_WAIT 内能收到并重发 ACK
🎤 面经
[网易]R1 #3170 TCP 三次握手、四次挥手
答:四步 FIN/ACK/FIN/ACK,全双工要双向关闭
[蚂蚁]R1 #6855 tcp 四次挥手,哪个环节可以连接重置?连接重置是什么?
答:任意时刻收到 RST 即重置;CLOSE_WAIT 卡住时常用 RST 强关
[蚂蚁]R1 #6843 TCP 四次挥手过程?LISTEN 是什么?CLOSE_WAIT 是什么?
答:FIN/ACK/FIN/ACK;LISTEN 监听新连接;CLOSE_WAIT 是被动方收到 FIN 后的状态
[Shopee]R1 #6459 TIME_WAIT 发生在哪?大量 TIME_WAIT 怎么解决?
答:主动关闭方;调小 fin_timeout、开 tw_reuse、改用长连接
[美团]R4 #3956 四次挥手的最后一个 ACK 的作用?为什么 TIME_WAIT 是 2MSL?
答:让对方收到 ACK 不重传;2MSL = 一来一回最大寿命,让旧包消亡
[腾讯]R5 #7124 TIME_WAIT 出现在哪一端?过多怎么办?
答:主动关闭方;调小 fin_timeout、tw_reuse、用长连接
[腾讯]R1 #5046 CLOSE_WAIT、TIME_WAIT 作用?什么时候出现?
答:CLOSE_WAIT 是被动方未 close 的中间态;TIME_WAIT 是主动方等 2MSL 兜底
[字节]R1 #4739 三次握手与四次挥手能不能改,为什么
答:三次是最少够用;挥手四次因为 ACK 和 FIN 之间隔 CLOSE_WAIT 期发剩余数据
tcp-reliable-transport
🎯 骨架
七大机制:序列号 + ACK 确认 + 超时重传 + 校验和 + 滑动窗口 + 流量控制 + 拥塞控制
序列号 + ACK:每字节一个序号,接收方 ACK 期望的下一个序号,去重 + 排序
超时重传 RTO:发出后定时器到了没收到 ACK 就重传;RTO 基于 RTT 动态估算
快重传:连续收到 3 个重复 ACK 不等超时立即重传(丢包侦测优化)
滑动窗口:接收方告知 rwnd 缓冲区剩余,发送方按窗口连续发不等单个 ACK
流量控制:接收方 rwnd 防止接收缓冲区被打爆(端到端限速)
拥塞控制:发送方 cwnd 防止压垮网络(慢启动 / 拥塞避免 / 快重传 / 快恢复)
⚠️ 易混淆
流量控制 vs 拥塞控制:前者保护接收方缓冲区(rwnd),后者保护中间网络(cwnd)
实际窗口 = min(rwnd, cwnd)
TCP 头部校验和只查头+伪头,链路层 CRC 才查全报文(双重保险)
重传只重传丢失的段,不是整窗口(选择性确认 SACK)
🎤 面经
[腾讯]R5 #7083 TCP 如何确保可靠传输?
答:序号校验 + 重排序 + 去重 + ACK + 超时重传 + 流量 + 拥塞控制
[腾讯]R1 #5014 TCP 如何确保可靠传输
答:数据包校验、重排序、丢弃重复、应答机制、超时重传、流量+拥塞控制
[快手]R2 #6332 tcp 怎么保证可靠性?
答:序号 + ACK + 超时重传 + 滑动窗口 + 流量 + 拥塞控制(七板斧)
[拼多多]R3 #6915 tcp 怎么保证有序传输?快速重传?TIME_WAIT?
答:序号排序;3 个重复 ACK 触发快重传;TIME_WAIT 让旧包消亡
[网易]R1 #3173 TCP 和 UDP 区别?保证通信的机制?
答:UDP 没保障;TCP 七板斧(序号/ACK/重传/窗口/流量/拥塞)
[拼多多]R3 #6953 HTTP 和 TCP 关系?TCP 如何保证可靠传输?
答:HTTP 跑在 TCP 上;可靠靠序号 + 确认重传 + 滑窗
[阿里]R5 #208 TCP 如何确保可靠传输?拥塞控制四阶段?
答:序号校验 + ACK 重传 + 流量;拥塞控制慢启动→拥塞避免→快重传→快恢复
[阿里]R4 #158 TCP 安全性是如何实现的?
答:可靠 ≠ 安全;可靠靠 7 板斧,安全靠 TLS(HTTPS 才有)
tcp-congestion-control
🎯 骨架
四阶段:慢启动 → 拥塞避免 → 快重传 → 快恢复
慢启动:cwnd 从 1 开始,每 RTT 翻倍(指数增长),到达 ssthresh 切到拥塞避免
拥塞避免:cwnd 每 RTT +1(线性增长),探测网络上限
超时丢包:ssthresh = cwnd/2,cwnd = 1,回到慢启动(剧烈降速)
快重传:连续 3 个重复 ACK,不等超时立即重传丢失段
快恢复:触发快重传时,ssthresh = cwnd/2,cwnd = ssthresh(不回到 1,温和降速)
流量控制(rwnd) vs 拥塞控制(cwnd):前者保护接收方,后者保护网络
⚠️ 易混淆
ssthresh = slow start threshold,慢启动阈值
超时重传走慢启动(保守);快重传走快恢复(积极)
实际发送窗口 = min(rwnd, cwnd)
BBR 算法 = Google 提的新拥塞控制,基于带宽和 RTT 估算,不靠丢包探测
🎤 面经
[阿里]R1 #82 TCP 拥塞控制和流量控制的目标?实现区别?
答:拥塞控制保护网络靠 cwnd;流量控制保护接收方靠 rwnd
[腾讯]R5 #7083 拥塞控制四阶段?
答:慢启动 → 拥塞避免 → 快重传 → 快恢复
[Shopee]R2 #998 TCP 拥塞控制怎么做?滑动窗口、慢启动、快恢复、快重传?
答:cwnd 指数增长到阈值切线性,丢包减半,3 个重复 ACK 触发快重传
[字节]R2 #286 TCP 的拥塞控制和慢启动怎么回事?
答:慢启动 cwnd 翻倍,到 ssthresh 切线性;丢包减半防止压垮网络
[拼多多]R3 #6956 TCP 流量控制?滑动窗口分哪几个区域?拥塞控制?
答:滑窗分已发已确认/已发未确认/可发未发/不可发;拥塞控制四阶段
[阿里]R4 #174 TCP 流量控制和拥塞控制
答:流量控制是端到端 rwnd;拥塞控制是网络全局 cwnd,两者取最小
[网易]R1 #3254 拥塞控制以及里面的算法?流量控制的协议?
答:慢启动/拥塞避免/快重传/快恢复;流量控制基于 rwnd 滑动窗口
[阿里]R5 #208 拥塞控制:慢开始、拥塞避免、快重传、快恢复
答:标准四阶段,超时回慢启动,3 个重复 ACK 进快恢复
tcp-vs-udp
🎯 骨架
TCP 面向连接(三次握手)+ 可靠 + 有序 + 头部 20B;UDP 无连接 + 不可靠 + 无序 + 头部 8B
TCP 通过序列号 + ACK + 重传 + 滑动窗口保证可靠;UDP 发了不管
TCP 适用:HTTP / 文件传输 / 数据库(数据完整性优先)
UDP 适用:视频直播 / 在线游戏 / DNS(延迟敏感、可丢少量)
视频直播用 UDP 原因:TCP 丢包重传冻结画面,UDP 丢帧只花一下
QUIC(HTTP/3 底层)= UDP + 用户态可靠传输,兼顾速度和可靠
应用层补可靠:游戏在 UDP 上做 ACK / 重传 / 序号自己管
⚠️ 易混淆
UDP 不是不能可靠,是协议本身不保证;可以应用层自己实现(QUIC、游戏协议)
DNS 默认走 UDP(53 端口),但响应 > 512 字节会回退 TCP
TCP 头部 20B 是固定部分,含选项最多 60B;UDP 头部固定 8B
🎤 面经
[百度]R2 #7165 TCP 和 UDP 的区别
答:连接 / 可靠 / 有序 / 头部大小四个维度,TCP 全保证 UDP 都不保
[小米]R3 #7217 UDP 可以用来视频传输,那怎么保证视频信号顺序
答:应用层加序号 + 缓冲重排 + 必要时 ACK 重传,参考 QUIC
[滴滴]R1 #2974 TCP 与 UDP 区别和应用场景,基于 TCP/UDP 的协议有哪些
答:TCP→HTTP/SMTP/FTP;UDP→DNS/DHCP/视频/游戏
[百度]R2 #7187 UDP 与 TCP 的区别,分别在什么情况下使用
答:完整性优先用 TCP,延迟敏感容忍丢包用 UDP
[蚂蚁]R1 #6843 TCP 和 UDP 有什么区别?描述一下 TCP 四次挥手的过程
答:连接 + 可靠 + 头部三大差异;TCP 有连接状态机 UDP 没有
[拼多多]R3 #6957 直播为什么用 UDP,王者荣耀用 UDP 会有什么问题(丢包)
答:丢包重传会卡顿;应用层自己做帧补偿、关键帧重传、抗抖动缓冲
[网易]R1 #3173 TCP 和 UDP 的区别,以及保证通信的机制有哪些
答:滑动窗口流量控制、慢启动拥塞控制、快重传快恢复
S3 HTTP 协议演进
http-version-comparison
🎯 骨架
演进主线:每代都在解决上一代的队头阻塞,HTTP/3 从传输层根治
HTTP/1.0:每请求新建 TCP 连接,握手开销大
HTTP/1.1:Keep-Alive 持久连接 + 管道化;遗留 HTTP 层队头阻塞(响应必须按序)
HTTP/2:二进制分帧 + 多路复用 + HPACK 头压缩 + Server Push;遗留 TCP 层队头阻塞
HTTP/3:QUIC(UDP + 用户态可靠传输),Stream 粒度独立重传,根治队头阻塞
HTTP/2 多路复用:多 Stream 在同一 TCP 连接上交错发送,响应可乱序返回
HTTP/3 额外收益:首连 1-RTT,再连 0-RTT,Connection ID 换网络不断连
⚠️ 易混淆
HTTP/1.1 管道化(pipeline)和 HTTP/2 多路复用不同:管道化是协议层串行发,响应按序;多路复用是帧级并行
HTTP/2 是二进制协议;HTTP/1.x 是文本协议
HTTP/2 没解决 TCP 层阻塞,因为 TCP 内核保证字节流有序,丢一个包后续都等
QUIC 在用户态,可热更新拥塞控制算法(BBR/Cubic)
🎤 面经
[蚂蚁]R2 #4123 http 1.1/2/3 区别
答:1.1 持久连接,2.0 二进制多路复用,3.0 QUIC 解决 TCP 队头阻塞
[腾讯]R2 #7143 http 1.1 与 2 的区别
答:文本→二进制帧;串行→多路复用;HPACK 头压缩;Server Push
[蚂蚁]R1 #6664 http1.0 和 2.0 的区别?哪个更新比较有意义?
答:1.0→1.1 加 Keep-Alive;1.1→2.0 多路复用最大跳变
[得物][R1] HTTP 1.0/1.1/2.0 的区别?
答:连接复用 + 队头阻塞 + 头部压缩三个维度
[腾讯][R1] HTTP/1.1 与 HTTP/2 的区别?
答:文本→二进制帧,串行→多路复用
[字节][R1] HTTP 能不能基于 UDP?
答:HTTP/3 已经这么做了,QUIC 在用户态补上可靠传输
[百度]R2 #2836 说一下滑动窗口,短连接和长连接?
答:HTTP/1.0 默认短连接,1.1 默认长连接 Keep-Alive 复用 TCP
[拼多多]R3 #6953 HTTP 和 TCP 的关系
答:HTTP 是应用层跑在 TCP 上;HTTP/3 改为跑在 UDP(QUIC)
http-methods-status-codes
🎯 骨架
常用方法:GET(查)/ POST(增)/ PUT(全量改)/ PATCH(部分改)/ DELETE(删)/ HEAD(只查头)/ OPTIONS(探测)
GET vs POST:GET 幂等无 body 通常带参数在 URL,POST 不幂等带 body;语义不同(查 vs 改)
状态码五大类:1xx 信息 / 2xx 成功 / 3xx 重定向 / 4xx 客户端错 / 5xx 服务端错
高频码:200 OK / 301 永久重定向 / 302 临时重定向 / 304 协商缓存命中 / 400 参数错 / 401 未认证 / 403 无权限 / 404 不存在 / 500 服务端崩 / 502 网关错 / 503 不可用 / 504 网关超时
301 vs 302:301 永久(浏览器记忆)/ 302 临时(每次重新请求)
401 vs 403:401 没登录或 token 失效 / 403 登录了但没权限
500 vs 502 vs 504:500 应用代码崩 / 502 网关收到下游错误响应 / 504 网关等下游超时
⚠️ 易混淆
GET 也可以带 body(HTTP 协议未禁止),但很多代理/服务器会丢弃,实践上不用
POST 不是不能查询,是语义不符合(不能被缓存/书签)
304 不算错误,是协商缓存命中,body 为空
502 不一定是后端错,也可能是 nginx 配置错把响应当错误
🎤 面经
[蚂蚁]R1 #6844 常见 HTTP 状态码?301 和 302 区别?504 和 500 区别?
答:301 永久 / 302 临时;500 应用错 / 504 网关等下游超时
[滴滴]R3 #2555 HTTP 是有状态还是无状态?200、404、500 都什么含义?
答:HTTP 无状态;200 成功 / 404 资源不存在 / 500 服务端崩
[Shopee]R1 #2788 HTTP 各种返回码,401 和 406 啥区别?
答:401 未认证(要登录);406 不可接受(Accept 协商失败)
[字节]R1 #454 HTTP 请求头,状态码,301、302、401、403?
答:301 永久重定向 / 302 临时;401 没登录 / 403 没权限
[字节]R1 #4836 网络状态码
答:1xx/2xx/3xx/4xx/5xx 五大类,重点 200/301/302/304/401/403/404/500
[百度]R2 #5752 了解哪些 HTTP 状态码
答:按五大类讲,重点说 304 协商缓存、502/504 网关相关
[字节]R1 #380 HTTP Method 有哪些?GET 和 POST 区别?GET 能不能传 body?
答:GET/POST/PUT/DELETE 等;GET 幂等查询;GET 协议允许 body 但实践不用
[网易]R1 #1640 HTTP 状态码
答:五大类一句话讲完,重点高频码(200/301/304/401/403/404/500/502/504)
S4 安全传输
https-tls-handshake
🎯 骨架
HTTPS = HTTP + TLS,解决三个问题:加密(防窃听)+ 完整性(防篡改)+ 身份认证(防中间人)
TLS 1.2 握手 4 步:ClientHello(带随机数A + 套件)→ ServerHello(带随机数B + 证书)→ Pre-Master Secret(公钥加密)→ 双方计算会话密钥
会话密钥 = PRF(Pre-Master Secret, 随机数A, 随机数B),三个随机数派生出对称密钥
用非对称加密换对称密钥:因为非对称慢 100~1000 倍,只用一次换密钥,后续走对称
数字证书验证:客户端用 CA 公钥验证证书签名,确认服务端身份;浏览器内置根 CA
TLS 1.3 优化:握手 1-RTT(合并 Hello + 密钥交换),Session Resumption 0-RTT
单向认证 vs 双向认证:HTTPS 默认只验服务端(单向);金融/IoT 用双向
⚠️ 易混淆
TLS 握手在 TCP 三次握手之后
HTTPS 不会让 HTTP 变快,只增加握手开销;HTTP/2 多路复用才让感知"变快"
数字签名 = 哈希(报文) 用私钥加密;验证用公钥解后比对哈希
证书里包含的是服务端公钥 + CA 签名,不是私钥(私钥在服务端本地)
🎤 面经
[腾讯]R1 #5024 HTTPS 握手过程中,对称和非对称加密如何结合?
答:用非对称加密传对称密钥(Pre-Master),后续通信用对称
[百度]R2 #7188 HTTPS 加密过程?对称和非对称的区别?
答:握手用非对称换对称密钥;非对称安全慢,对称快但要协商密钥
[蚂蚁]R1 #6824 Http 和 Https 的区别?Https 加密方式?
答:HTTPS = HTTP + TLS;混合加密(非对称换密钥 + 对称传数据)
[Shopee]R1 #1194 https 和 http、握手相关问题、安全问题
答:HTTPS 解决加密+完整性+身份;握手 4 步换密钥;CA 防中间人
[字节]R1 #4741 https 如何实现可靠传输?需要几次握手?
答:可靠靠下层 TCP;TLS 1.2 多 2-RTT,1.3 优化到 1-RTT
[百度]R1 #2849 https 为什么安全?证书认证怎么认证?证书内容?
答:握手验证证书 + 协商密钥;证书含公钥 + 域名 + CA 签名
[蚂蚁]R1 #6704 安全协议有哪些?https 是啥?
答:TLS/SSL/IPSec;HTTPS = HTTP + TLS,端口 443
[滴滴]R1 #2517 HTTPS 和 HTTP 对比;CA 数字证书;对称非对称过程?
答:HTTPS 加 TLS;CA 证书防伪造;混合加密兼顾速度与安全
symmetric-asymmetric-encryption
🎯 骨架
对称加密:同一把密钥加解密,代表 AES(128/192/256 位),速度快(纳秒级)
非对称加密:公钥 + 私钥,公钥加密只有私钥能解,代表 RSA / ECC,慢 100-1000 倍
对称缺点:密钥怎么安全送给对方
非对称缺点:太慢,不适合大数据量;安全性靠大数分解难题(RSA)/椭圆曲线(ECC)
实战混合加密:用非对称换对称密钥(一次),后续大量数据走对称(HTTPS 模式)
数字签名:发送方用私钥加密报文哈希,接收方用公钥解开比对(防篡改 + 防抵赖)
数字证书:CA 用自己的私钥给"公钥+域名"签名,浏览器用 CA 公钥验签确认身份
⚠️ 易混淆
对称加密 ≠ Hash:对称可逆(解密能拿原文),Hash 不可逆
ECC 比 RSA 短得多但同等强度(256 位 ECC ≈ 3072 位 RSA)
公钥可以公开,私钥必须保密;公钥加密只能私钥解,私钥签名只能公钥验
AES 是对称代表;RSA 是非对称代表;MD5/SHA 是哈希算法
🎤 面经
[百度]R2 #7188 对称加密和非对称加密的区别?
答:对称同一把密钥快但难传密钥;非对称双密钥安全但慢
[字节]R1 #4821 对称加密与非对称加密算法的区别?常见非对称算法?
答:AES 对称 vs RSA/ECC 非对称;非对称密钥对生成靠大数难题
[滴滴]R1 #2517 对称加密非对称加密过程?常见加密算法?
答:对称 AES/DES;非对称 RSA/ECC;HTTPS 混合用
[网易]R1 #1637 几种加密算法;对称加密,非对称加密;https
答:MD5 哈希 / AES 对称 / RSA 非对称;HTTPS 三者配合
[字节]R1 #455 https 原理,数字签名,数字证书,非对称加密算法过程?
答:握手用 RSA 换密钥;签名用私钥+哈希防篡改
[滴滴]R4 #4791 HTTPS 和 HTTP 对比;CA 数字证书;对称非对称过程
答:HTTPS 混合加密;CA 验证身份防中间人
[Shopee]R1 #5409 对称加密和非对称加密
答:核心区别是密钥数量;对称快非对称慢,混合用
[腾讯]R1 #5024 HTTPS 握手中对称和非对称如何结合?
答:非对称握手换 Pre-Master(一次),对称传数据(每次)
digital-certificate
🎯 骨架
数字证书 = 服务端公钥 + 域名 + 有效期 + CA 用私钥的签名(证明是真的)
证书链:服务端证书 ← 中间 CA ← 根 CA;浏览器内置根 CA 公钥
验证流程:客户端用上级 CA 公钥验下级证书签名,递归到根 CA 即合法
防中间人:中间人没有 CA 私钥,签不出合法证书;自签名证书浏览器会报警
证书撤销:CRL(撤销列表)/ OCSP(在线查询),过期或私钥泄露时撤销
证书内容(X.509):版本/序列号/签名算法/颁发者/有效期/主体/公钥/扩展(SAN)
自签证书:自己当 CA 给自己签,仅内网/测试可用,正式必须 Let's Encrypt 等公开 CA
⚠️ 易混淆
证书里只有服务端公钥,私钥永远不离开服务端
CA 签名是对"公钥+域名"哈希后用 CA 私钥加密,验证用 CA 公钥
证书过期 ≠ 不安全,过期是失效;撤销才是出问题
HTTPS 单向认证(验服务端)vs 双向认证(双方都验,金融/IoT)
🎤 面经
[百度]R1 #2849 https 为什么安全,证书认证怎么认证,证书内容?
答:CA 签证书 + 浏览器验签;证书含公钥+域名+CA 签名
[字节]R1 #455 https 原理,数字签名,数字证书,非对称加密算法过程?
答:握手验证证书链,确认服务端身份;签名 = 私钥加密哈希
[滴滴]R1 #2517 CA 数字证书;对称非对称过程?
答:CA 用根证书给服务端公钥签名,客户端用 CA 公钥验签
[滴滴]R4 #4791 HTTPS 和 HTTP 对比;CA 数字证书
答:CA 是受信任第三方,给服务端公钥背书,防伪造
[百度]R2 #2820 https 怎么保证数据的完整性?
答:证书验身份 + 握手协商密钥 + 每段数据带 MAC 防篡改
[腾讯]R1 #5024 HTTPS 握手中对称和非对称如何结合?
答:用证书里的服务端公钥加密 Pre-Master,私钥才能解
[Shopee]R1 #1194 https 和 http、握手相关、安全问题
答:握手核心是验证书 + 换密钥;CA 是信任根
[蚂蚁]R1 #6704 安全协议有哪些?https 是啥?
答:TLS 协议;HTTPS 用证书做身份认证防中间人
S5 应用层实战
cookie-session-token
🎯 骨架
HTTP 无状态,需要会话标识;三种方案:Cookie / Session / Token (JWT)
Cookie:服务端 Set-Cookie 浏览器存,每次请求自动带上;存客户端,明文有篡改风险
Session:服务端存会话状态,给客户端发 SessionID(通常存 Cookie);服务端有状态
Token (JWT):服务端签发,自包含(header.payload.signature),无需存服务端
分布式 Session 三方案:粘性会话(按 IP hash 路由)/ 复制(同步多节点)/ 集中存储(Redis 共享)
JWT 优点:无状态可水平扩展;缺点:吊销难、payload 不能放敏感信息(Base64 不是加密)
安全配置:Cookie HttpOnly(防 XSS)+ Secure(仅 HTTPS)+ SameSite(防 CSRF)
⚠️ 易混淆
Cookie 是存储机制,Session 是服务端状态机制,二者常配合(SessionID 存在 Cookie 里)
JWT 不是加密的,是签名的,Base64 解开就能看到 payload
Token 一般指 JWT 或 OAuth2 access_token;不要泛指
SessionID 泄露 = 完整冒充;JWT 泄露 = 也能冒充,吊销要靠黑名单
🎤 面经
[阿里]R4 #160 cookie 和 session
答:Cookie 存客户端、Session 存服务端;Cookie 常用来存 SessionID
[网易]R1 #1563 Cookie 和 Session 的联系和区别
答:Session 服务端有状态;Cookie 客户端存储;二者配合实现会话
[网易]R1 #5576 JWT 和 cookie/session 有什么区别
答:JWT 无状态自包含;session 有状态依赖服务端
[得物]R1 #3709 别人拿到 sessionId 就能冒充登录,怎么解决?
答:HttpOnly+Secure+SameSite;绑定 IP/UA;定期重置;HTTPS
[拼多多]R3 #1281 session 放哪里?分布式 session 怎么管理?
答:粘性会话/Session 复制/Redis 集中存储(推荐)
[阿里]R1 #87 分布式 Session 一致性方案?哪种更适合高并发?
答:Redis 集中存储 + Token 无状态化最适合高并发
[快手]R2 #6300 为什么用 JWT 不用 redis 的 token?
答:JWT 自包含无 IO;Redis token 多一次网络但易吊销
[京东]R2 #5953 JWT 吊销怎么办?无存储的方案?
答:吊销靠黑名单/短过期+刷新 token;纯无存储难以即时吊销
dns-resolution
🎯 骨架
DNS = Domain Name System,把域名翻译成 IP(A 记录),跑在 UDP 53 端口
八步查询:浏览器缓存 → 系统缓存 → hosts 文件 → 本地 DNS(运营商)→ 根域名 → 顶级域 → 权威 DNS → 拿到 IP
客户端只问本地 DNS 一次(递归查询);本地 DNS 一级一级问根/顶级/权威(迭代查询)
UDP 优先:DNS 数据小(< 512B),UDP 省去握手;响应超 512B 回退 TCP
记录类型:A(IPv4)/ AAAA(IPv6)/ CNAME(别名)/ MX(邮件)/ NS(域名服务器)/ TXT
缓存 TTL:减少查询;改 DNS 后生效慢就是因为各级缓存
CDN 调度:基于 DNS 的负载均衡,按用户 IP 返回最近节点
⚠️ 易混淆
递归查询(客户端→本地 DNS)和迭代查询(本地 DNS→根/顶级/权威)是两种
hosts 文件优先级最高,可以覆盖 DNS
DNS 走 UDP 但区域传送(zone transfer)走 TCP
DNS 劫持 = 篡改返回 IP;DNS over HTTPS(DoH)能防
🎤 面经
[蚂蚁]R1 #6827 Http 请求过程,DNS 解析过程?三次握手和四次握手?
答:DNS 八步定位 IP → TCP 握手 → HTTP;DNS 走 UDP 53
[字节]R3 #957 DNS 用途?解析过程?迭代和递归区别?UDP 还是 TCP?
答:域名转 IP;客户端递归本地 DNS 迭代;UDP 主,超 512B 回退 TCP
[百度]R2 #2833 说一下 DNS 解析服务
答:八级缓存 + 递归 + 迭代查询;UDP 53 端口
[Shopee]R1 #924 说说 dns?除了查 ip 还可以查什么?哪些类型记录?
答:A/AAAA/CNAME/MX/NS/TXT 等;除 IP 还有邮件、别名
[滴滴]R3 #2556 URL 整个解析过程(DHCP/ARP/RIP/OSPF/BGP/DNS/TCP)
答:DHCP 拿本机 IP → ARP 拿 MAC → DNS 拿域名 IP → TCP 握手
[京东]R1 #5504 输入 url 全流程,让某域名不访问这个 ip 怎么办?
答:改 hosts 优先级最高 / 改 DNS A 记录 / 防火墙 DROP 目标 IP
[蚂蚁]R1 #6843 建立一个 socket 连接要经过哪些步骤?
答:DNS 解析 → TCP 三次握手(含序列号交换)
[通用] DNS 的 TTL 有什么用?
答:缓存有效期;减少查询提速,改 DNS 后等 TTL 才全网生效
DDD
S1 贫血到充血
ddd-anemic-vs-rich
🎯 骨架
贫血模型:实体只有 getter/setter,业务逻辑全在 Service 层(面向过程)
充血模型:实体含业务行为,状态和行为绑在一起;Service 只做编排(面向对象)
DDD 推崇充血:领域规则就近封装,避免散落 Service 各处
判断标准:只涉及一个实体状态变更 → 实体方法;多聚合协调 → 应用服务
例:order.cancel() 放 Order 实体(自身状态变更);"取消订单后退款"放应用服务(跨聚合)
贫血并非一无是处:简单 CRUD 业务用贫血够用,强用充血是过度设计
实战常见反模式:DTO 当实体用、Service 几百行业务规则、所有规则散落 if-else
⚠️ 易混淆
充血 ≠ 复杂:充血是逻辑放对地方,不是把实体写得很重
贫血模型 + Util 工具类 不算充血
实体方法不是任意可调用,要受聚合根入口约束
充血常和不变性(值对象)配合使用
🎤 面经
[美团]R1 #3412 领域模型设计,如何防止贫血模型?怎么区分值对象和领域实体?
答:业务规则放实体方法里;实体有 ID 生命周期,值对象无 ID 不可变
[Shopee]R2 #992 DDD 中实体和值对象的区别?
答:实体有唯一标识;值对象只看属性值相等可替换
[Shopee]R1 #1191 讲讲 DDD 架构。优缺点?
答:充血模型是 DDD 核心;优点业务集中可测;缺点学习成本高
[腾讯]R3 #1955 DDD 在项目中的应用?
答:核心域充血模型;非核心域贫血够用;不教条
[蚂蚁]R4 #3435 了解 DDD 吗?项目中的领域模型?
答:充血实体 + 聚合根;规则就近不散落 Service
[拼多多]R1 #3402 面向对象和面向过程的区别 ddd 设计
答:贫血是面向过程;充血才是真 OO;DDD 推崇充血
[网易]R1 #1567 了解 DDD 架构吗?
答:充血模型是战术核心;让代码反映业务
[B站]R3 #3374 项目介绍 项目难点 ddd 设计
答:贫血到充血改造的难点;团队认知 + 边界识别
ddd-entity-valueobject
🎯 骨架
实体(Entity):有唯一标识 ID,生命周期内 ID 不变;用 ID 判断相等
值对象(Value Object):无唯一标识,看属性值判断相等;不可变(immutable)
实体例:Order / User / Product;值对象例:Money(金额+币种)/ Address / DateRange
值对象可自由替换:旧 Address 直接换新 Address,无需变更引用
实体可变,值对象不可变(all final 字段,所有方法都返回新对象)
共享 vs 独占:值对象可被多实体共享(不可变所以安全);实体不能跨聚合共享
选型判断:是否需要历史追溯/独立标识 → 实体;只关心属性值 → 值对象
⚠️ 易混淆
有 ID 不等于实体:临时聚合或快照可能有 ID 但是值对象语义
值对象不可变是关键:Java 写时所有字段 final + 不提供 setter
不可变值对象天然线程安全
Money 经常被误设计成实体(带 ID),其实属性值相同就该相等
🎤 面经
[Shopee]R2 #992 DDD 中实体和值对象的区别?
答:实体有 ID 看身份;值对象无 ID 看属性值,不可变可替换
[美团]R1 #3412 怎么区分值对象和领域实体?
答:实体有唯一标识有生命周期;值对象只看属性值相等
[阿里]R1 #139 聚合根怎么设计?
答:聚合根是入口实体;内部含其他实体和值对象
[蚂蚁]R4 #3435 项目中有哪些领域模型?
答:每个上下文有聚合根(实体)+ 值对象(如金额、地址)
[Shopee]R1 #1191 DDD 架构优缺点?
答:充血实体 + 值对象是战术核心;让模型表达更丰富
[腾讯]R3 #1955 DDD 在项目中的应用?
答:值对象常用于金额/地址/时间区间,让代码更安全
[拼多多]R1 #3402 面向对象 ddd 设计
答:值对象不可变是 OO 重要实践
[B站]R3 #3374 项目难点 ddd 设计
答:识别哪些是实体哪些是值对象是落地难点
ddd-layered-architecture
🎯 骨架
四层:用户接口层 / 应用层 / 领域层 / 基础设施层
用户接口层(Interfaces):Controller + DTO + Assembler,负责请求接收和数据转换
应用层(Application):ApplicationService 编排领域服务,管理事务(@Transactional 在这层)
领域层(Domain):实体 + 值对象 + 聚合根 + 领域服务 + Repository 接口;核心业务规则
基础设施层(Infrastructure):Repository 实现 + MQ + 外部调用 + 配置;技术细节
依赖倒置:Repository 接口在领域层,实现在基础设施层;外层依赖内层
与三层架构对比:业务逻辑下沉到领域层;事务管理上浮到应用层;数据访问做依赖倒置
⚠️ 易混淆
@Transactional 放应用层,不放领域服务
Repository 是接口归属于领域层;实现归属于基础设施层
领域层不能依赖基础设施层(典型违规:领域类直接 import MyBatis)
简单业务别套四层,会过度设计
🎤 面经
[Shopee]R2 #992 DDD 中 Service 和 Repository 的分层?
答:Service 分应用服务(编排)和领域服务(规则);Repository 接口在领域,实现在基础设施
[Shopee]R1 #1191 讲讲 DDD 架构。优缺点?
答:四层 + 依赖倒置;优点业务集中可测;缺点学习成本和代码量
[腾讯]R3 #1955 DDD 在项目中的应用?
答:核心域走四层;非核心走传统三层
[蚂蚁]R4 #3435 微服务拆分原则?项目领域模型?
答:每服务用 DDD 四层;领域层是核心
[美团]R1 #3107 DDD 没践行到底?
答:四层不是教条;要权衡复杂度
[美团]R1 #3412 领域模型设计如何防止贫血?
答:业务规则下沉领域层;应用层只编排
[B站]R3 #3374 项目难点 ddd 设计
答:四层依赖方向 + 依赖倒置是落地点
[拼多多]R1 #3402 面向对象 ddd 设计
答:领域层是 OO 核心;其他层是技术适配
S2 战术设计
ddd-aggregate-root
🎯 骨架
聚合根 = 一组相关对象的入口,外部只能通过聚合根访问内部对象
聚合是事务一致性的边界——一个事务只修改一个聚合
聚合尽量小:只包含必须保持一致性的对象,过大会并发冲突严重
聚合间通过 ID 引用,不直接持有对象引用
跨聚合的操作通过领域事件实现最终一致性
例:Order(聚合根)→ OrderItem(聚合内实体)+ ShippingAddress(值对象)
存储:关系型分表 + 外键关联适合查询;MongoDB 整聚合一文档读写快
⚠️ 易混淆
聚合根 ≠ 主表:聚合根是 DDD 概念,强调一致性边界
不是所有实体都是聚合根,只有暴露给外部的入口实体才是
聚合内对象的修改必须通过聚合根方法(如 order.addItem())
Payment / Logistics 不应在 Order 聚合内,应是独立聚合
🎤 面经
[阿里]R1 #139 聚合根怎么设计?为什么这么设计?存关系型还是非关系型?
答:一致性入口,聚合尽量小;关系型适合查询,文档型适合整聚合读写
[蚂蚁]R4 #3435 你们项目中有哪些领域模型?
答:每个限界上下文有聚合根(订单/商户/支付/物流域各有自己的)
[Shopee]R2 #992 DDD 中实体和值对象的区别?Service 和 Repository 的分层?
答:实体有 ID,值对象无 ID 不可变;Service 编排,Repository 持久化
[美团]R1 #3412 领域模型设计,怎么区分值对象和领域实体?
答:实体有唯一标识生命周期,值对象只看属性值相等可替换
[腾讯]R3 #1955 DDD 在项目中的应用?
答:用聚合根做一致性边界;事件风暴识别聚合
[拼多多]R1 #3402 面向对象和面向过程的区别 ddd 设计
答:聚合根承担业务规则,避免贫血面向过程
[B站]R3 #3374 项目介绍 项目难点 ddd 设计
答:限界上下文 + 聚合根 + 充血模型三件套
[网易]R1 #1567 了解 DDD 架构吗?
答:聚合根是核心战术单元,承担一致性 + 业务规则封装
ddd-domain-service
🎯 骨架
领域服务(Domain Service)= 不属于任何单一实体的领域逻辑
出现条件:业务规则涉及多个聚合协调,放任何一个实体里都不合适
例子:转账(涉及两个账户)、订单创建(涉及商品/库存/优惠券多个域)
与应用服务(Application Service)的区别:领域服务含业务规则;应用服务只编排
与实体方法的区别:单聚合的规则在实体方法;跨聚合的规则在领域服务
命名约定:以业务动词命名(OrderCreationService、TransferService)
不要滥用:把贫血的 Service 改个名字叫"领域服务"是误用
⚠️ 易混淆
领域服务 vs 应用服务:前者写规则,后者写编排
领域服务 vs 工具类:领域服务有业务语义,工具类是技术辅助
单聚合的规则不要拆出领域服务,放实体里更内聚
领域服务无状态,不要存字段
🎤 面经
[Shopee]R2 #992 DDD 中 Service 和 Repository 的分层?
答:领域服务封跨聚合规则;应用服务编排;Repository 持久化
[other]R1 #4431 抽象出领域服务,物流和售后为啥可以单独抽象?
答:跨聚合的核心业务能力 → 领域服务承担
[蚂蚁]R4 #3435 微服务拆分原则?项目中的领域模型?
答:每个上下文有自己的领域服务;不要写大而全的中台服务
[腾讯]R3 #1955 DDD 在项目中的应用?
答:聚合根 + 领域服务 + 应用服务三层
[美团]R1 #3412 领域模型设计?
答:实体内聚业务规则;跨实体协调用领域服务
[Shopee]R1 #1191 DDD 架构优缺点?
答:领域服务让跨聚合规则有归属;缺点学习成本高
[B站]R3 #3374 项目介绍 项目难点 ddd 设计
答:识别什么放实体什么放领域服务是难点
[通用] 领域服务和工具类的区别?
答:领域服务有业务语义在领域层;工具类是技术辅助跨层
ddd-domain-event
🎯 骨架
领域事件 = 领域内发生的、对其他部分有意义的事件(OrderCreated / PaymentSucceeded)
用途:跨聚合解耦 + 跨上下文通信 + 集成第三方系统 + 异步处理
实现方式:进程内(Spring ApplicationEvent)/ 跨服务(MQ)
与领域命令的区别:命令是请求(DoX);事件是事实(XHappened)
一致性策略:聚合内同步处理保证强一致;跨聚合用事件最终一致
事件存储:本地事务里同时插入事件表,定时扫描发送(事务消息保最终一致)
滥用陷阱:所有跨聚合操作都用事件 → 事件链长、排查难、性能差
⚠️ 易混淆
领域事件 ≠ 系统日志:前者是业务概念,后者是技术辅助
领域事件 ≠ 集成事件:领域事件是域内业务事实,集成事件是跨域通信用
事件不是消息队列特有,进程内也可以
强一致诉求不要用事件,用同步调用
🎤 面经
[other]R1 #4431 售后产生的物流信息属哪个域?
答:物流域;售后通过领域事件触发,物流域消费创建物流单
[蚂蚁]R4 #3435 微服务拆分原则?项目中的领域模型?
答:跨聚合用事件解耦;同聚合用聚合根方法
[腾讯]R3 #1955 DDD 在项目中的应用?
答:领域事件实现限界上下文之间的通信
[Shopee]R1 #1191 DDD 架构优缺点?
答:事件解耦让上下文独立;缺点是排查复杂
[美团]R1 #3107 DDD 为什么没践行到底?
答:事件链长是落地难点;强一致用同步
[other]R1 #4431 抽象出领域之后业务定制如何实现?
答:领域事件 + 扩展点;让核心稳定、定制隔离
[B站]R3 #3374 项目介绍 项目难点 ddd 设计
答:领域事件 + 事务消息保证最终一致是常见组合
[通用] 进程内事件 vs MQ 事件如何选?
答:单服务内强相关用进程内;跨服务/异步用 MQ
ddd-repository
🎯 骨架
Repository = 仓储,提供聚合根的持久化访问,让领域层无视底层存储
接口在领域层,实现在基础设施层(依赖倒置)
操作单位是聚合根,不是单个表(Order 仓储 save 时同时持久化 OrderItem)
与 DAO 的区别:DAO 是表级访问;Repository 是聚合级访问
命名约定:OrderRepository(按聚合根命名),不是 OrderItemRepository
实现技术:MyBatis / JPA / 自研 ORM;可同时读写不同 DB(写 MySQL 读 ES)
反模式:Repository 暴露 SQL / 返回不完整聚合 / 跨聚合查询
⚠️ 易混淆
Repository 接口 ≠ Mapper 接口:前者面向聚合根,后者面向表
一个聚合根一个 Repository,不要按表拆 Repository
复杂查询不应该用 Repository(会破坏聚合语义),应该走 CQRS 查询模型
Repository 不返回 DTO,返回领域对象
🎤 面经
[Shopee]R2 #992 DDD 中 Service 和 Repository 的分层?
答:Repository 接口在领域层,实现在基础设施层;操作聚合根
[Shopee]R1 #1191 讲讲 DDD 架构。优缺点?
答:Repository 是依赖倒置的关键;让领域层不依赖技术实现
[蚂蚁]R4 #3435 项目领域模型?
答:每聚合根一个 Repository;不按表拆
[腾讯]R3 #1955 DDD 在项目中的应用?
答:Repository 隔离持久化技术;可换 DB 不影响业务代码
[阿里]R1 #139 聚合根存关系型还是非关系型?
答:选型不影响 Repository 接口;接口稳定,实现可换
[美团]R1 #3412 领域模型设计?
答:Repository 操作聚合根整体;保证内部一致性
[Shopee]R2 #992 实体和值对象的区别?Repository 分层?
答:Repository 只为实体(聚合根);值对象通过聚合根存取
[通用] Repository 和 DAO 的区别?
答:DAO 表级技术接口;Repository 聚合级业务接口
S3 战略设计与落地
ddd-bounded-context
🎯 骨架
限界上下文(Bounded Context)= 一个明确的边界,边界内领域模型含义一致
解决问题:同一个词在不同业务下含义不同("商品"在商品域是 SKU,在订单域是订单行,在物流域是包裹)
不是简单按业务模块分,是按"语言边界"分——同一概念在不同域有不同含义就要拆
拆分方法:事件风暴 → 找到聚合 → 强相关聚合归同一上下文
一个限界上下文 ≈ 一个微服务(粗略对应,不是绝对)
上下文之间通过 API / 领域事件通信,不共享数据库
边界识别原则:高内聚(边界内强相关)+ 低耦合(边界外弱相关)
⚠️ 易混淆
限界上下文不只是微服务边界,单体里也可以有多个上下文
同一个名词不同域可以有不同模型(不要建"通用 Product 模型")
上下文边界 ≠ 数据库边界:可以同库不同 schema
边界识别错误是 DDD 落地最大痛点
🎤 面经
[蚂蚁]R4 #3435 微服务拆分的原则?了解 DDD 吗?
答:按限界上下文划分;事件风暴识别边界
[腾讯]R3 #1955 DDD 在项目中的应用?
答:限界上下文 + 聚合根 + 上下文映射三件套
[美团]R1 #3107 你用了 DDD,为什么没践行到底?订单写在交易域不符合 DDD?
答:边界要权衡;强耦合可同服务但代码包隔离
[Shopee]R1 #1191 讲讲 DDD 架构。优缺点?
答:限界上下文是战略核心;优点边界清晰,缺点学习成本
[小米]R1 #1935 微服务改造的领域划分是怎么做的?
答:事件风暴 + 限界上下文 + 团队对齐
[other]R1 #4431 售后产生的物流信息属于物流域还是售后域?
答:物流信息属物流域;触发动作属售后域;事件解耦
[other]R1 #4431 通用能力抽象领域服务(物流、售后)
答:识别核心域 + 通用域 + 支撑域
[B站]R3 #3374 项目介绍 项目难点 ddd 设计
答:限界上下文识别是难点;用事件风暴 + 团队反复对齐
ddd-context-mapping
🎯 骨架
上下文映射 = 限界上下文之间的关系模式,描述边界如何对接
共享内核(Shared Kernel):两上下文共享部分模型,强耦合,慎用
客户-供应商(Customer-Supplier):上游提供 API,下游消费,下游有发言权
防腐层(ACL):下游用适配器隔离上游模型变化,避免污染
开放主机服务(OHS):上游对所有下游提供标准化 API(如开放平台)
已发布语言(Published Language):用统一格式(JSON Schema 等)传递信息
顺从者(Conformist):下游被迫接受上游模型(弱势方),通常加 ACL 缓解
⚠️ 易混淆
上下文映射不是技术拼接,是组织协作模式
防腐层是其中一种映射模式,不是上下文映射的全部
共享内核虽然便利但牺牲独立性,团队边界要考虑
不是所有边界都需要严格映射,看耦合度选
🎤 面经
[蚂蚁]R4 #3435 微服务拆分原则?项目中的领域模型?
答:限界上下文 + 上下文映射;典型用客户-供应商 + 防腐层
[other]R1 #4431 抽象出领域之后业务定制内容如何实现?
答:上下文映射 + 扩展点;防止核心域被定制污染
[腾讯]R3 #1955 谈谈对 DDD 的看法及应用?
答:战略层核心是限界上下文 + 上下文映射
[Shopee]R1 #1191 讲讲 DDD 架构
答:战略战术两层;战略层包含上下文映射
[美团]R1 #3107 用 DDD 为什么没践行到底?
答:边界识别和上下文映射是难点,不是教条
[other]R1 #4431 通用能力抽象领域服务,物流信息属哪个域?
答:物流域是供应商,售后是客户;事件解耦
[B站]R3 #3374 项目介绍 项目难点 ddd 设计
答:上下文映射 + 团队对齐是难点
[小米]R1 #1935 微服务改造的领域划分?
答:先识别上下文,再定义上下文映射模式
ddd-anticorruption-layer
🎯 骨架
防腐层 = Anti-Corruption Layer,下游通过适配器隔离上游模型变化
解决问题:调用上游服务时不让上游 DTO/概念污染本域模型
实现方式:本域定义自己的领域对象 + 适配器(Adapter)转换上游 DTO
适用场景:调用遗留系统 / 调用第三方 API / 上游模型混乱不可控
优势:上游变更不影响本域代码(改适配器就行);本域语义清晰
代价:多一层转换有性能成本;要写适配代码
一种特殊上下文映射模式:用于客户-供应商关系中下游保护
⚠️ 易混淆
防腐层 ≠ DAO 层:防腐层是上下文边界保护,DAO 是数据访问
防腐层 ≠ Wrapper:防腐层有业务转换语义,Wrapper 只是封装调用
不是所有外部调用都要 ACL:稳定可控的内部服务可以不用
防腐层在六边形架构中体现为适配器(Adapter)
🎤 面经
[other]R1 #4431 通用能力抽象领域服务,售后产生的物流信息属于哪个域?
答:物流信息属物流域;售后通过事件触发;ACL 隔离物流模型
[蚂蚁]R4 #3435 微服务拆分原则?了解 DDD 吗?
答:限界上下文划分;ACL 处理上游污染
[腾讯]R3 #1955 DDD 在项目中的应用
答:限界上下文 + 防腐层是战略层核心
[Shopee]R1 #1191 DDD 架构的优缺点
答:架构清晰可演进;防腐层避免上游污染本域
[美团]R1 #3107 你用了 DDD 为什么没践行到底?
答:边界要权衡;不是所有跨域都要 ACL
[other]R1 #4431 抽象出领域之后,业务定制内容如何实现?
答:核心域稳定 + 防腐层隔离 + 扩展点处理变化
[通用] 防腐层和门面模式(Facade)的区别?
答:Facade 是简化调用接口;ACL 是模型语义转换
[通用] 为什么调用第三方一定要加 ACL?
答:第三方接口不可控;ACL 把变化封锁在适配器内
ddd-microservice-split
🎯 骨架
拆分方法:事件风暴 → 识别聚合 → 划分限界上下文 → 一个上下文 ≈ 一个服务
事件风暴(Event Storming):团队梳理业务流程中的所有领域事件
拆分原则:按业务边界拆而非技术分层;高内聚低耦合
实战常见坑:拆太细(调用链长)/ 拆太粗(小单体)/ 数据库没拆(共享 DB 改表影响多服务)
拆服务后:服务间通过 API + 领域事件通信,不共享 DB
渐进式拆:先包隔离再服务隔离;同一仓库不同 module 也算
何时拆:业务复杂度高 + 团队规模大 + 长期演进时拆;简单业务别强拆
⚠️ 易混淆
微服务拆分 ≠ 必须用 DDD:DDD 是其中一种方法论
限界上下文边界 ≠ 数据库边界:可以同库不同 schema 过渡
拆分前要先识别核心域 / 通用域 / 支撑域,核心域优先拆
一个上下文是否对应一个服务还要考虑团队、性能、演进策略
🎤 面经
[蚂蚁]R4 #3435 微服务拆分的原则?了解 DDD 吗?项目领域模型?
答:限界上下文划分 + 事件风暴 + 团队对齐
[蚂蚁]R4 #3424 单点服务拆分微服务后怎么数据平滑迁移?
答:双写 → 影子读 → 灰度切流 → 清理旧库
[字节]R1 #7324 微服务拆分的原则?服务间通信用 RPC 还是消息队列?
答:DDD 上下文拆;强一致 RPC,最终一致用 MQ
[美团]R1 #3107 DDD 为什么没践行到底?订单写在交易域?
答:边界要权衡;强耦合可代码包隔离
[B站]R1 #5834 微服务是怎么拆分的?有哪些独立的微服务?
答:按限界上下文拆;列出本项目核心域
[蚂蚁]R3 #4199 你们的服务怎么拆分的?为什么这样拆?
答:DDD 上下文 + 业务边界;数据库随服务一起拆
[滴滴]R1 #3022 服务划分策略
答:按限界上下文 + 高内聚低耦合
[滴滴]R3 #2528 拆分读服务是微服务的什么思想?
答:CQRS 思想;读写分离不同模型
S4 演进与权衡
ddd-cqrs-eventsourcing
🎯 骨架
CQRS = Command Query Responsibility Segregation,读写分离两种模型
Command 模型:处理写操作,走聚合根 + 领域规则;Query 模型:直接查 DB/ES,跳过领域层
解决问题:复杂查询不适合走聚合根(性能差、跨聚合查询难)
实现:写库(关系型)+ 读库(ES/MongoDB);写完发事件同步到读库
事件溯源(Event Sourcing)= 不存当前状态,存所有领域事件,按需回放
事件溯源优点:完整审计追溯 + 可任意时间点重建状态
事件溯源代价:所有查询都要回放(用快照 + 增量优化);学习成本高,难落地
⚠️ 易混淆
CQRS 不一定要事件溯源,也可同库不同模型
事件溯源 ≠ 领域事件:前者是存储模式,后者是通信机制
简单业务用 CQRS 是过度设计,CRUD 不适合
读写分离 ≠ CQRS:传统主从读写分离是数据库层,CQRS 是模型层
🎤 面经
[腾讯]R3 #1955 DDD 在项目中的应用?
答:CQRS 用于复杂查询场景;不是所有 DDD 都要 CQRS
[Shopee]R1 #1191 讲讲 DDD 架构。优缺点?
答:CQRS 是 DDD 进阶模式;适合读写差异大场景
[蚂蚁]R4 #3435 你们项目中有哪些领域模型?
答:核心域用充血聚合;查询场景用 CQRS 走 ES
[美团]R1 #3107 用 DDD 没践行到底
答:CQRS、事件溯源是高阶模式,简单场景不适用
[B站]R3 #3374 项目介绍 项目难点 ddd 设计
答:复杂查询用 CQRS;写聚合根读 ES 是常见组合
[other]R1 #4431 通用能力抽象,业务定制内容如何实现?
答:CQRS 让查询模型独立,定制不影响写
[通用] CQRS 适用场景?
答:读写差异大、查询复杂、追求高性能查询的场景
[通用] 事件溯源在金融业务的应用?
答:账户余额变动审计;按时间回放对账
ddd-overdesign-tradeoff
🎯 骨架
DDD 不是银弹,简单 CRUD 强用 DDD 是过度设计
不该用 DDD:简单 CRUD 业务 / 团队不熟 / 项目周期短 / 业务规则简单
该用 DDD:业务复杂多状态流转 / 系统长期演进 / 多团队并行开发
落地坑1:贫血模型惯性,规则散落 Service
落地坑2:聚合设计过大,并发冲突严重
落地坑3:限界上下文边界模糊,跨域 JOIN 查询破坏边界
落地策略:核心域用 DDD;非核心域用传统三层;不强求一刀切
⚠️ 易混淆
DDD 学习成本高 ≠ DDD 难:是团队认知和实践经验问题
过度设计的标志:3 行 if-else 能写完的逻辑搞了 5 个类
团队不熟时强推 DDD 反而代码更乱
重构成 DDD 要分阶段,不要一次性大改
🎤 面经
[美团]R1 #3107 你用了 DDD,为什么没践行到底?
答:边界要权衡;不是教条;强耦合可代码包隔离
[other]R1 #4431 项目架构演变?通用能力抽象?
答:先做核心域 DDD;非核心域三层;渐进演进
[腾讯]R3 #1955 DDD 在项目中的应用?团队认知问题?
答:核心域试点跑通后再推广;不是所有业务都需要
[Shopee]R1 #1191 DDD 架构优缺点?
答:优点业务集中可测;缺点学习成本和过度设计风险
[B站]R3 #3374 项目难点 ddd 设计
答:团队认知 + 边界识别 + 渐进落地三大难点
[蚂蚁]R4 #3435 微服务拆分原则?了解 DDD 吗?
答:DDD 是手段不是目的;按业务复杂度选
[拼多多]R1 #3402 面向对象 ddd 设计
答:DDD 是 OO 进阶;简单业务别套
[小米]R1 #1935 微服务改造领域划分?
答:先核心域试点;学习曲线 + 渐进推广
SYSTEM-DESIGN
S1 分布式基础理论
cap-base-theory
🎯 骨架
系统设计前必问:业务对一致性敏感度?决定 CP 还是 AP
CP 适用:钱、库存、支付、订单状态(错一笔都不行)→ ZK / Etcd / 强一致 DB
AP 适用:商品列表、推荐、关系链、计数(短暂不一致用户感知不强)→ Cassandra / Eureka
BASE 是 AP 的工程化:基本可用(降级)/ 软状态(中间不一致)/ 最终一致
实战工具:异步消息 + 重试 + 幂等 + 对账,把 CP 业务退化为 BASE
强一致代价:降低可用性 + 增加延迟 + 减少吞吐
系统设计回答模式:"业务要 X 一致性,所以选 Y 方案,权衡是 Z"
⚠️ 易混淆
CAP 中"C" ≠ ACID 中"C":前者是线性一致,后者是约束一致
误以为 CP 系统更好:高可用业务(电商列表)选 AP 才合理
BASE 不是降低标准,是分布式场景下务实的折中
单机 ACID + 多机 BASE 是常见组合(数据库强一致 + 整体最终一致)
🎤 面经
[蚂蚁]R1 #6693 ACID CAP BASE 理论
答:单机 ACID;分布式 CAP 三选二(P 必选);BASE 是 AP 工程化
[滴滴]R3 #3003 分布式的 cap、base 理论,什么是柔性状态?
答:CAP 三选二;BASE 软状态 = 允许中间不一致
[蚂蚁]R4 #4185 cap base,强弱一致用什么方法?
答:强一致用 Paxos/Raft;最终一致用消息+本地表+对账
[字节]R5 #299 对 CAP、BASE 的理解
答:CAP 是设计指南;BASE 是落地手段;按业务选 CP/AP
[百度]R2 #5640 cap 属性
答:核心是分区时舍弃哪个;P 必选,二选一
[京东]R3 #6472 CAP 理论是什么?consul 是 AP 还是 CP?
答:Consul 默认 CP;强一致优先
[蚂蚁]R4 #4157 去中心化协议符合 CAP 哪些?
答:gossip 是 AP 弱一致;不适合强一致场景
[拼多多]R4 #1097 cap 了解么
答:常考三个字母含义 + CP/AP 例子
S2 流量治理
rate-limiting-algorithms
🎯 骨架
四大算法:固定窗口 / 滑动窗口 / 漏桶 / 令牌桶
固定窗口:[0,1s] 计数 → 达阈值拒绝;简单但边界突发(2 倍)
滑动窗口:把 1s 切片(如 100ms × 10 格),按格累加;平滑边界
漏桶:请求入桶按固定速率出,超容量拒绝;严格匀速保护下游
令牌桶:固定速率发令牌,请求拿令牌通过;桶有容量允许突发
漏桶 vs 令牌桶:漏桶限制下游处理速率;令牌桶限制自己 QPS 但允许偶发突发
选型:网关入口用令牌桶(友好用户);下游保护用漏桶(防压垮)
⚠️ 易混淆
固定窗口的"两倍突发"经典问题:[0.99s, 1.01s] 内可能放过 2 倍量
漏桶不允许突发:即使桶有空间也按固定速率放
令牌桶允许短时突发:桶里积累的令牌可一次消耗完
Sentinel 默认是滑动窗口;Guava RateLimiter 是令牌桶
🎤 面经
[快手]R2 #6392 常见限流算法?令牌桶与漏桶区别?
答:4 种算法;漏桶严格匀速,令牌桶允许突发
[B站]R1 #5836 令牌桶为什么不用加锁?还知道哪些限流算法?
答:Redis 原子;其他有滑动窗口/漏桶/计数器
[网易]R1 #1480 哪些限流算法?滑动窗口和令牌桶的区别?
答:滑窗按时间整形;令牌桶按速率允许突发
[other]R1 #4391 流量突增 100 倍如何解决?为啥令牌桶能应对突发?
答:桶容量积令牌;突发时消耗已有令牌;漏桶不行
[蚂蚁]R3 #4726 令牌桶真的能限流么?
答:能,配速率和桶容量;分布式用 Redis
[字节]R3 #989 实现简单令牌桶?加滑动窗口要求?
答:标准令牌桶 + 滑窗结合;用 Redis Lua
[携程]R1 #2914 限流算法
答:4 种算法;按 QPS 控制 vs 速率控制选
[B站]R1 #3313 如何实现一个限流器?
答:单机 Guava;分布式 Redis + Lua 滑窗
token-bucket-vs-leaky-bucket
🎯 骨架
漏桶:请求入桶按固定速率出(漏水),超容量拒绝;严格匀速
令牌桶:固定速率发令牌入桶,请求拿令牌通过;桶有容量
漏桶:保护下游(出口速率严格固定,不论上游多猛)
令牌桶:保护自己(限自己 QPS,但允许短时突发消耗已积令牌)
突发流量:漏桶完全不允许;令牌桶允许桶容量内的突发
实现:漏桶用队列+定时器;令牌桶用计数器+时间戳算补充
落地:Guava RateLimiter(令牌桶,常用)/ Nginx limit_req(漏桶模式)
⚠️ 易混淆
同样是"桶",行为相反:漏桶是出口控制,令牌桶是入口控制
漏桶不允许突发是绝对的:哪怕桶空也按速率出
令牌桶突发能力 = 桶容量;超过容量的突发也会被限流
选型:要保护下游严格匀速选漏桶;要给用户体验留点突发空间选令牌桶
🎤 面经
[快手]R2 #6392 令牌桶与漏桶的区别?
答:漏桶严格匀速保护下游;令牌桶允许突发保护自己
[other]R1 #4391 为啥令牌桶可以应对突发?漏桶有突发会怎么样?
答:令牌桶有桶容量积令牌;漏桶严格匀速不允许
[B站]R1 #5836 令牌桶为什么不用加锁?还知道哪些限流算法?
答:原子计数;其他有滑动窗口、漏桶
[网易]R1 #1480 滑动窗口和令牌桶的区别?
答:滑窗按时间整形;令牌桶按速率允许突发
[蚂蚁]R3 #4726 桶令牌真的能限流么?
答:能,但要根据下游能力调速率和桶容量
[字节]R3 #989 实现简单令牌桶?
答:记录上次刷新时间 + 当前令牌数,每次取时计算补充
[字节]R2 #4872 限流怎么做的?说一说原理?
答:令牌桶原理;网关 Sentinel + 业务接口限流
[携程]R1 #2914 限流算法
答:4 种算法对比;漏桶/令牌桶是两种主流速率控制
distributed-rate-limiting
🎯 骨架
单机限流:Guava RateLimiter(令牌桶)/ Sentinel 单机模式
分布式限流:Redis + Lua 脚本(原子性保证)/ Sentinel 集群模式(专门 token-server)
Redis 滑动窗口实现:用 ZSet score=时间戳,移除窗口外的,count 当前窗口内
Redis 令牌桶实现:用 STRING 存当前令牌数 + 上次刷新时间;Lua 脚本算补充令牌
网关层限流:Spring Cloud Gateway / Nginx limit_req(单机)/ APISIX
业务层限流:Sentinel 注解 / AOP 切面 + Redis
限流维度:QPS / 并发数 / IP / 用户 ID / 接口;多维度组合
⚠️ 易混淆
单机限流不能水平扩展(每台都按自己 QPS 算,全局会超)
分布式限流必须用 Redis + Lua 保证原子,不能 GET 后 SET
集群限流(token-server)和分布式限流(Redis)是两种实现思路
令牌桶和漏桶适合不同场景;选错可能全限掉或全放过
🎤 面经
[B站]R1 #5836 令牌桶为什么不用加锁?还知道哪些限流算法?
答:Redis 单线程天然原子;其他有滑动窗口、漏桶
[字节]R3 #989 实现简单令牌桶?滑动版限制 1 小时不超 5 万?
答:Redis ZSet 滑动窗口;时间戳为 score
[字节]R2 #4872 限流怎么做的?说一说原理?
答:网关 Sentinel + 业务层接口限流;底层 Redis + Lua
[蚂蚁]R2 #4116 限流原理,有无读/写过相关实现
答:单机 Guava;分布式 Redis Lua;维度多种组合
[小红书]R1 #4070 多线程安全限流组件,根据下游错误率自适应?
答:滑动窗口 + 自适应阈值;BBR 思路按下游延迟调整
[other]R1 #4503 滑动窗口限流、Lua 脚本怎么实现?
答:ZSet 时间戳 + Lua 脚本原子裁剪 + 计数 + 加新点
[携程]R1 #5278 实现一个本地限流器(IP + QPS)
答:Guava RateLimiter / 自实现令牌桶;Map
[携程]R1 #2914 限流算法
答:4 种算法 + 单机/分布式两种部署
circuit-breaker
🎯 骨架
熔断 = 类比保险丝,下游故障时主动切断调用,防止雪崩
三态机:Closed(正常)→ Open(断开,直接失败)→ Half-Open(试探,放部分请求)
触发条件:错误率超阈值(如 50%)或超时率达标,进入 Open 状态
Half-Open:Open 状态过 N 秒进入,放少量请求探测,成功则回 Closed,失败回 Open
隔离方式:线程池隔离(Hystrix 经典)/ 信号量隔离(轻量级)
主流框架:Hystrix(已停更)/ Sentinel(阿里)/ Resilience4j(替代 Hystrix)
配套机制:降级(fallback 返回兜底数据)/ 限流 / 重试 / 超时
⚠️ 易混淆
熔断 ≠ 限流:熔断是被动反应(下游挂),限流是主动控制(自己流量大)
熔断 ≠ 降级:熔断决定打不打,降级决定打不通时返回什么
隔离防止雪崩传播;熔断防止持续失败拖垮自己
Sentinel 既支持限流也支持熔断,统一资源治理
🎤 面经
[腾讯]R2 #3145 熔断是怎么实现的?
答:三态机 Closed/Open/HalfOpen,错误率超阈值断开
[小米]R3 #7222 提供服务者挂了怎么办?
答:feign + Hystrix 熔断降级;返回 fallback 兜底
[字节]R2 #4870 熔断机制,怎么实现?
答:Hystrix/Sentinel;错误率统计 + 状态机切换 + 半开探测
[美团]R3 #1226 怎么理解高可用?熔断机制怎么实现?
答:高可用 = 限流 + 熔断 + 降级 + 隔离;熔断三态机
[字节]R1 #460 Hystrix 半开状态?隔离怎么实现?
答:HalfOpen 放部分请求探测;线程池/信号量隔离
[蚂蚁]R1 #6706 从 hystrix 一路问到原理、自己如何实现、如何优化
答:核心三态机 + 滑动窗口统计 + 半开试探
[携程]R1 #7250 下游服务扛不住、批量重试导致雪崩,怎么预防?
答:熔断 + 降级 + 重试退避 + 限流,组合防御
[京东]R3 #2067 你们的熔断降级怎么做的?
答:网关层 Sentinel 配规则 + 业务代码 fallback 兜底
load-balancing
🎯 骨架
分层:DNS(地域)→ 接入层 LVS/F5(4 层)→ 网关 Nginx(7 层)→ 服务层 Ribbon/Dubbo(客户端)
4 层(LVS):基于 IP+端口,性能高(百万 QPS),不看 HTTP 内容
7 层(Nginx):基于 URL/Header/Cookie,灵活,QPS 万级
客户端 LB(Ribbon/Dubbo):从注册中心拿服务列表,自己挑
服务端 LB(Nginx):所有请求过中间节点,再分发
算法:轮询 / 加权轮询 / 随机 / 加权随机 / 最少连接 / 一致性哈希 / 最快响应
健康检查 + 自动摘除:负载均衡的可用性基础
⚠️ 易混淆
4 层 vs 7 层:性能 vs 灵活;TCP/IP 层 vs HTTP 层
客户端 LB vs 服务端 LB:去中心化 vs 中心化
LVS 三种模式:DR(直接路由,最快)/ NAT / TUN
一致性哈希主要用在缓存路由、会话粘性,不是默认 LB 算法
🎤 面经
[腾讯]R2 #3142 微服务的特点,如何实现服务发现和负载均衡?
答:注册中心 + 客户端 LB(Ribbon);算法多种
[蚂蚁]R1 #6902 负载均衡怎么做?
答:分层 LVS + Nginx + Ribbon;按 QPS 和功能选
[拼多多]R3 #6965 负载均衡策略?一致性哈希?虚拟节点?
答:5+ 种策略;一致性哈希适合会话粘性
[百度]R2 #7203 dubbo 负载均衡策略?
答:Random/RoundRobin/LeastActive/ConsistentHash 4 种
[腾讯]R3 #3140 负载均衡算法实现,轮询和随机的缺点?
答:轮询无差别;随机分布不均;用加权或最少连接
[B站]R1 #5835 如何做负载均衡?nginx?
答:Nginx 7 层 + LVS 4 层;nginx 配 upstream 加权轮询
[蚂蚁]R2 #4119 负载均衡策略
答:常见 5 种;按业务选(无状态轮询,会话哈希)
[京东]R3 #3086 单机演变为分布式,从前端负载到后端详细描述?
答:CDN → DNS → LVS → Nginx → Ribbon → 服务层
S3 分布式基础设施
distributed-lock
🎯 骨架
三种实现:DB 锁 / Redis 锁(AP)/ ZK 锁(CP)
DB 锁:唯一索引 / SELECT FOR UPDATE 行锁;性能差,DB 压力大,仅小流量
Redis 锁:SET key val NX EX + Lua 释放 + Redisson watchdog 续期;高性能 AP
ZK 锁:临时顺序节点 + watch 前序节点;强一致 CP,性能差
选型:高 QPS 用 Redis(够用);金融强一致用 ZK;小流量直接 DB
RedLock:跨 N 个独立 Redis 实例,半数加锁成功;防单实例故障,但工程争议大
锁四要素:互斥 + 防死锁(过期时间)+ 防误删(value 唯一)+ 自动续期
⚠️ 易混淆
Redis 锁默认 AP,主从切换可能丢锁
ZK 锁强一致但慢;不是高 QPS 场景的选择
watchdog ≠ RedLock;前者解决业务超时,后者解决主节点单点
锁粒度要细(按业务 ID 加锁),不要全局大锁
🎤 面经
[蚂蚁]R4 #3430 redis 和 zk 分布式锁分别有什么优缺点?
答:Redis 高性能 AP 可能丢锁;ZK 强一致 CP 性能低
[百度]R2 #7198 Redis 分布式锁如何保证原子性?
答:SET NX EX 一条命令;Lua 脚本释放
[腾讯]R1 #3108 如何防止 A 线程释放 B 的锁?
答:value 唯一标识,释放前比对再删
[小米]R1 #3162 分布式锁、锁过期、锁误删、锁超时
答:四要素全要解决;watchdog + 唯一 value + Lua
[快手]R2 #2807 MySQL 实现分布式锁?还有更好的方式?
答:DB 锁性能差;Redis/ZK 更好,按一致性需求选
[携程]R1 #2896 分布式锁怎么实现?什么时候加?加在什么上?
答:临界区加 + 按业务 ID 细粒度;优先 Redisson
[B站]R1 #2757 怎么实现分布式锁保证数据唯一?
答:Redis SET NX EX + Lua + Redisson 自动续期
[美团]R1 #3096 Redisson 分布式锁源码?
答:底层 Lua + hash 重入 + watchdog 续期 + Netty 时间轮
distributed-id
🎯 骨架
需求:全局唯一 + 趋势递增(B+ 树友好)+ 高性能 + 信息安全
UUID:简单无依赖,但 36 字节字符串、随机性导致 B+ 树页分裂、无序
数据库自增:单点瓶颈;Flickr 方案是 N 个 DB 错开步长(M1: 1,3,5...)
号段模式:DB 取一段(10000 个)放本地 + 双 buffer 预加载,美团 Leaf-segment 经典
雪花算法:64 位 = 时间戳 + 机器 ID + 序列号;趋势递增、无中心
Redis INCR:单线程原子递增;性能好但有持久化丢失风险
选型:业务无序用 UUID;分库分表用雪花/号段;订单号用业务前缀+雪花
⚠️ 易混淆
趋势递增 ≠ 严格递增:雪花在不同机器或同毫秒序列内不严格递增
UUID v1(基于 MAC+时间)有序,但泄露 MAC;v4(随机)安全无序
雪花的时钟回拨问题是经典坑
美团 Leaf 双模式:segment(号段)+ snowflake(雪花)
🎤 面经
[腾讯]R2 #3146 id 生成器怎么实现的?如何全局递增?
答:雪花算法趋势递增;严格全局递增用号段或 Redis
[蚂蚁]R1 #6745 拆分后主键怎么保证唯一?Snowflake 全局递增唯一吗?
答:雪花保证唯一;趋势递增不严格;时钟回拨要处理
[蚂蚁]R1 #6811 分库分表后怎么解决全局递增?
答:雪花算法 / 美团 Leaf / Redis INCR
[网易]R1 #1624 唯一 ID 怎么生成?
答:雪花 / UUID / 号段 / Redis INCR;按需选
[other]R1 #4424 全局 ID 如何做的?时钟回拨问题?
答:雪花 + workerID 分配 + 时钟回拨处理(等待 / 切 worker)
[other]R1 #4452 主键如何实现?为啥不用 redis 自增?
答:Redis 有持久化丢失风险;雪花无中心化更可靠
[字节]R1 #4831 基于 k8s 的自动雪花算法
答:StatefulSet 序号 = workerID,自动对应扩缩容
[字节]R2 #487 雪花算法;如何处理系统时间不一致?
答:时钟回拨 NTP 同步 + 等待 / 切换 workerID
snowflake-algorithm
🎯 骨架
64 位结构:1 符号 + 41 时间戳 + 10 机器 ID(5+5)+ 12 序列号
性能:1000ms × 4096 = 400 万 ID/秒/节点;趋势递增对 B+ 树友好
三大问题:时钟回拨 / workerID 分配 / 长度溢出(10 位 = 1024 节点)
时钟回拨方案:等待回拨时长 / 切换 workerID / 抛异常
workerID 分配:手动配 / IP hash / ZK / 配置中心 / K8s StatefulSet 序号
美团 Leaf 改进:Leaf-segment 号段(DB 取段)/ Leaf-snowflake(ZK 分 workerID)
业务定制:自定义结构(业务前缀 + 时间戳 + 机器 + 序列),常见 19 位订单号
⚠️ 易混淆
趋势递增 ≠ 严格递增:跨节点不严格
10 位 = 1024 节点上限是硬限制,超大型公司要定制结构
workerID 不能用 IP 末段,IP 重复时冲突
时钟回拨在容器漂移、跨机房同步时容易出现
🎤 面经
[腾讯]R2 #3146 id 生成器怎么实现的?如何全局递增?
答:雪花趋势递增;严格全局用号段或 Redis INCR
[美团]R1 #4910 雪花算法生成 ID 的逻辑
答:64 位结构;趋势递增;机器 ID 唯一性是关键
[字节]R3 #397 雪花算法原理
答:时间戳 + 机器 + 序列;时钟回拨是经典坑
[网易]R1 #1608 雪花算法的原理
答:64 位结构 + 趋势递增 + 时钟回拨处理
[字节]R2 #487 雪花算法;如何处理系统时间不一致?
答:等待回拨 / 切 worker / 抛异常
[other]R1 #4424 时钟回拨问题?
答:记录最大时间戳防回退 + 等待 / 切换
[other]R1 #4452 雪花算法多少位?什么情况会重复?
答:18-19 位;时钟回拨 + workerID 冲突会重复
[字节]R1 #4831 k8s 自动雪花算法机器中心?
答:StatefulSet 序号自动 = workerID
consistent-hashing
🎯 骨架
应用场景:分库分表 / 分布式缓存 / CDN 调度 / 服务路由 / 负载均衡
解决问题:节点扩缩容时大多数 key 映射不变,只迁移 K/N 数据
哈希环:0~2^32-1 环形空间,节点和 key 都映射上去;key 顺时针找首个节点
虚拟节点:每物理节点 150-200 个虚拟节点散布环上,避免数据倾斜
Redis Cluster 选哈希槽(16384)不选一致性哈希:槽迁移更灵活、定位 O(1)
Memcached 标准用一致性哈希:客户端选节点,无中心协调
Dubbo 一致性哈希负载均衡策略:相同参数请求路由到同一提供者,会话粘性
⚠️ 易混淆
一致性哈希迁移粒度是"节点之间的弧",不是固定比例
虚拟节点数量影响均匀性:太少倾斜,太多 hash 计算开销大
一致性哈希适合无状态分布;分库分表场景常用范围/取模
哈希槽 vs 一致性哈希:槽是预分配静态映射,可以手动迁移
🎤 面经
[蚂蚁]R1 #6818 谈一谈 Hash 的一致算法
答:哈希环 + 虚拟节点;扩缩容只迁 K/N
[拼多多]R3 #6965 你说轮询负载均衡,还知道其他策略?一致性哈希?虚拟节点?
答:常见策略 5 种;一致性哈希适合会话粘性;虚拟节点解决倾斜
[百度]R2 #7204 普通 hash 加新机器后果?一致性 hash?
答:普通 hash 全量迁移;一致性哈希只迁 K/N
[阿里]R4 #2964 memcached 集群存放数据原理?一致性哈希?
答:客户端用一致性哈希定位节点,无中心
[滴滴]R4 #5442 一致性 hash
答:哈希环 + 虚拟节点核心
[字节]R1 #7317 Redis 集群分片?一致性哈希和哈希槽区别?
答:Cluster 用 16384 槽预分配;不用一致性哈希
[拼多多]R1 #857 什么是一致性哈希?
答:负载均衡策略之一,节点变动只迁 K/N
[腾讯]R3 #3140 负载均衡算法实现,轮询和随机的缺点?
答:轮询无差别;一致性哈希支持会话粘性
distributed-transaction
🎯 骨架
五大方案(强→弱):XA(2PC) / TCC / Saga / 事务消息 / 本地消息表
XA:标准 2PC,DB 层强一致,性能差,长事务占锁
TCC:应用层 Try-Confirm-Cancel,强一致但侵入大;适合金融
Saga:长事务+反向补偿;适合多步业务流(订单+物流+发券)
事务消息:RocketMQ 半消息+回查;最常用的最终一致方案
本地消息表:本地事务里同时插业务表和消息表,定时扫描发送
Seata 框架:统一接入 4 种模式(AT/TCC/XA/Saga),最常用 AT 模式(自动 SQL 回滚)
⚠️ 易混淆
AT 模式 = Seata 自动版的 2PC,对业务零侵入但性能比 TCC 差
选型核心:业务能不能接受最终一致?能就用消息,不能就 TCC/XA
TCC 三接口必须幂等(防重试)+ 处理空回滚悬挂
不要为所有跨服务操作都用分布式事务,能拆就拆(最终一致 + 对账)
🎤 面经
[美团]R1 #3093 2PC、3PC、Seata、TCC 这些方案及缺点?
答:从强到弱排列;TCC 侵入大;Seata 框架统一
[蚂蚁]R1 #6717 两阶段、TCC、本地消息、MQ 事务、Saga
答:5 种方案;按一致性强弱选;常用消息+本地表
[腾讯]R2 #3122 分布式事务的实现
答:方案有 5 种;按业务选型;Seata 是落地框架
[腾讯]R1 #5086 2pc、3pc、tcc、seata、本地事件表+mq
答:考全方案对比;Seata AT 是默认推荐
[other]R1 #4360 如何保证数据一致性?TCC 还有其他方案?
答:消息最终一致 + 业务幂等 + 对账兜底
[other]R1 #4470 Seata 各模式特点?什么情况只能用 TCC?
答:AT 自动 SQL;TCC 资源预留型;XA 强一致;Saga 长流程
[蚂蚁]R4 #4185 2pc 大概过程?
答:Prepare 投票 + Commit/Rollback 决议
[字节]R2 #467 2PC 缺点和解决办法?
答:阻塞 + 单点;改 TCC 或最终一致
S4 系统设计方法论
system-design-methodology
🎯 骨架
四步法:需求澄清 → 容量估算 → 架构设计 → 难点深挖
需求澄清:业务场景 / QPS / 数据量 / 一致性要求 / SLA / 扩展性
容量估算:日活 × 人均请求 / 86400 = QPS;峰值 = 平均 × 3-5;存储 = 每条 × 总量
架构分层:客户端 → CDN → 接入层(LB/网关)→ 应用层 → 缓存 → 数据层(DB/MQ/搜索)
横向扩展四件套:负载均衡 / 服务无状态 / 数据分片 / 缓存
高可用三件套:限流 / 熔断 / 降级 + 多机房容灾 + 监控告警
数据一致性策略:CP(强一致)/ AP(最终一致)/ 缓存双写一致性
⚠️ 易混淆
不要一上来就讲技术栈,先问清需求
QPS 和 TPS 不同:QPS 是查询,TPS 是事务(含写)
系统设计没有标准答案,重点是权衡
高可用不只是堆机器,要有降级预案
🎤 面经
[腾讯]R2 #3151 秒杀系统的架构设计
答:流量漏斗 + 削峰 + 预扣库存 + 异步落 DB
[百度]R2 #7172 积分排行榜场景,怎么设计这个系统?
答:Redis ZSet 排行榜 + DB 持久化 + 定时同步
[拼多多]R3 #6969 设计 IM 系统,保证消息不丢、不乱序?
答:序号 + ACK + 离线消息 + 长连接 + 顺序投递
[快手]R2 #6380 上来就让设计一个小型社交系统
答:从 ER 到架构;用户/动态/关系/Feed 流分模块
[小米]R1 #3657 URL 短链系统,依据 url 查 random 值
答:62 进制编码 + Redis 缓存 + DB 持久;hash 冲突处理
[网易]R1 #3281 100 亿数据实时展示和查找?分库分表?数据迁移?
答:分片键设计 + ES 搜索 + 双写迁移 + 灰度切流
[拼多多]R3 #6932 B 端圈选服务的瓶颈?双机房 + 监控告警 + 多种降级
答:高可用三件套 + 多机房 + 监控告警
[蚂蚁]R1 #6896 设计秒杀系统
答:核心三点(高并发、防超卖、防刷)+ 流量漏斗
S5 经典系统设计题
seckill-system-design
🎯 骨架
三大目标:高并发(百万 QPS)/ 防超卖 / 防黄牛刷子
流量漏斗:CDN(静态资源)→ 限流(接入层)→ 排队(消息队列)→ 库存扣减
防超卖:Redis 预扣库存(DECR 原子)+ 异步落 DB;DB 用 update set stock=stock-1 where stock>0
削峰:消息队列 RocketMQ/Kafka,秒杀请求转异步处理
防刷:验证码 + 限流(IP/UID)+ 黑名单 + 答题门槛
缓存预热:开始前把商品和库存预热到 Redis;用本地缓存 + 多级缓存
兜底:Redis 挂了用 DB(降级);消息堆积用动态扩容消费者
⚠️ 易混淆
库存扣减不能直接 update stock 不加判断(可能扣到负)
Redis 预扣 + 异步落 DB 是常见架构,要对账(防 Redis 挂 DB 不一致)
排队 ≠ 限流:限流是直接拒绝,排队是慢慢放
秒杀页面要静态化,避免每个请求都走 DB
🎤 面经
[携程]R1 #2895 秒杀库存如何设计?如何防超卖?
答:Redis 预扣 + DB 兜底;update where stock>0
[携程]R1 #2880 秒杀如何防止超卖
答:原子操作 + 库存乐观锁 + 限流防过量
[腾讯]R2 #3151 秒杀系统的架构设计
答:流量漏斗(CDN→网关限流→MQ 削峰→Redis 预扣→DB)
[蚂蚁]R1 #6896 设计秒杀系统
答:核心三点:高并发、防超卖、防刷
[京东]R3 #3087 设计一个秒杀系统?
答:分层防御 + 消息队列削峰 + Redis 预扣 + 异步落 DB
[字节]R2 #4870 设计一个秒杀系统?高并发和超卖问题?redis 也扛不住怎么办?
答:分片缓存 + 本地缓存 + 限流降级 + 动态扩容
[字节]R1 #7322 从前端到后端完整描述秒杀系统设计
答:CDN→静态化→网关→限流→排队→预扣→落库
[字节]R1 #565 秒杀系统注意点,超卖怎么办?
答:原子库存 + 限流 + MQ 削峰;超卖了对账补单或退款
DISTRIBUTED
S1 分布式基石
cap-base-theory
🎯 骨架
CAP:分布式系统三选二,一致性 Consistency / 可用性 Availability / 分区容错性 Partition tolerance
P 是网络分区不可避免(机房断网/网线挂了),所以工程实践上只能在 CP 和 AP 之间选
CP 代表:ZooKeeper / Etcd / HBase(强一致优先,分区时拒绝服务)
AP 代表:Eureka / Cassandra / DynamoDB(可用优先,分区时接受不一致)
BASE = Basically Available(基本可用)+ Soft State(软状态)+ Eventually Consistent(最终一致)
BASE 是 AP 的工程化扩展:服务降级 / 中间状态 / 最终一致替代强一致
业务选择:交易/账户走 CP(钱不能错);列表/推荐走 AP(暂时不一致用户感知不强)
⚠️ 易混淆
CAP 中的"C"是线性一致性(Linearizability),不是数据库 ACID 中的 C
"三选二"误读:P 必选,实际是 C 和 A 二选一
BASE 不是替代 ACID,是分布式场景下的折中
弱一致 / 最终一致 / 强一致是连续光谱,不是离散选项
🎤 面经
[蚂蚁]R1 #6693 ACID CAP BASE 理论
答:ACID 单机事务;CAP 分布式三选二(P 必选,CP/AP 二选一);BASE 是 AP 工程版
[滴滴]R3 #3003 说说分布式的 cap、base 理论,什么是柔性状态?
答:CAP 三选二;BASE 基本可用+软状态+最终一致;柔性状态 = 中间不一致
[蚂蚁]R4 #4185 cap 了解么?base 呢?强一致和弱一致用什么方法?
答:CAP 是抽象模型;强一致用 Paxos/Raft;最终一致用消息+本地表
[字节]R5 #299 说一下对 CAP、BASE 理论的理解
答:CAP 是设计权衡指南;实际 P 必选;BASE 是 AP 工程落地
[拼多多]R4 #1097 cap 了解么,分别指什么
答:C 一致性 / A 可用性 / P 分区容错;常考 CP vs AP 例子
[百度]R2 #5640 cap 属性
答:CAP 三个属性是分布式经典框架;P 必选,工程二选一
[京东]R3 #6472 CAP 理论是什么?consul 是 AP 还是 CP?
答:Consul 默认 CP(用 Raft 强一致);可配 AP
[蚂蚁]R4 #4157 去中心化协议符合 CAP 哪些?分布式应该 CP 还是 AP?
答:gossip 是 AP;选 CP/AP 看业务(交易 CP,列表 AP)
consistent-hashing
🎯 骨架
痛点:普通 hash(key)%N,N 变化时几乎所有 key 重新映射
一致性哈希:把 0~2^32-1 组成环,节点和 key 都映射到环上,key 顺时针找到第一个节点
"一致性"含义:节点增减时只有 K/N 个 key 重新映射(K 是 key 总数,N 是节点数)
数据倾斜问题:节点少或哈希分布不均时,某节点压力暴增
虚拟节点:每个物理节点对应多个虚拟节点(150~200 个),打散负载
应用场景:分库分表 / 分布式缓存(Memcached)/ CDN 调度 / Dubbo 负载均衡
Redis Cluster 用哈希槽(16384 槽)不用一致性哈希:槽预分配灵活迁移、定位 O(1)
⚠️ 易混淆
一致性哈希的"一致性" ≠ CAP 的 C,是"映射关系稳定"的意思
虚拟节点不是动态的副本,是静态映射
Redis Cluster 用槽(slot),Memcached 用一致性哈希
节点新增 → 只迁移落在新节点和上游节点之间的 key
🎤 面经
[蚂蚁]R1 #6818 谈一谈 Hash 的一致算法
答:哈希环 + 顺时针找节点;节点变化只影响 K/N
[拼多多]R3 #6965 一致性哈希,虚拟节点有啥用?
答:虚拟节点打散负载,避免数据倾斜
[百度]R2 #7204 普通 hash 加新机器后果?一致性 hash?
答:普通 hash 几乎全部 key 重映射;一致性哈希只迁移 K/N
[阿里]R4 #2964 memcached 集群存放数据的过程?一致性哈希实现原理?
答:客户端用一致性哈希定位节点;环上顺时针找
[滴滴]R4 #5442 一致性 hash
答:哈希环 + 虚拟节点;用于分布式缓存/分库分表
[字节]R1 #7317 Redis 集群数据如何分片?一致性哈希和哈希槽的区别?
答:Redis Cluster 用 16384 槽(不是一致性哈希);槽便于迁移
[拼多多]R1 #857 什么是一致性哈希(负载均衡)?
答:哈希环 + 虚拟节点;负载均衡的一种实现策略
[腾讯]R3 #3140 负载均衡算法实现;轮询和随机的缺点?
答:轮询不考虑节点能力;一致性哈希解决会话粘性
S2 分布式协调——锁
zab-protocol
🎯 骨架
ZAB = ZooKeeper Atomic Broadcast,ZK 的原子广播协议,类似 Raft
两种模式:崩溃恢复(选主)/ 消息广播(写请求)
写流程:Leader 收到写请求 → 广播 Proposal(PRE)→ 半数 Follower 回 ACK → Leader 广播 Commit
选主算法 FastLeaderElection:投票 (myid, ZXID),比较 ZXID 大、再比 myid 大
ZXID = 64 位(32 位 epoch + 32 位 counter),保证全局唯一递增
半数以上原则:写需要半数 ack 提交;选主需要半数同意当选
强一致:客户端读 leader 是最新数据;读 follower 可能旧(默认配置)
⚠️ 易混淆
ZAB ≠ Paxos:ZAB 是为高吞吐广播设计;Paxos 是通用一致性协议
ZAB 类似 Raft,但 ZAB 早于 Raft;Raft 更易理解
epoch(任期)切换时旧 leader 的未提交事务会被丢弃
半数原则导致网络分区时少数派不可写(CP)
🎤 面经
[滴滴]R1 #2520 ZAB 协议,ZK 选举过程,ZXID 结构?
答:ZAB 类 Raft;选主比 ZXID 再比 myid;ZXID 32+32 位
[字节]R1 #461 zab 协议原理,半数原则,zk 属于哪种一致性?
答:半数 ack 提交;ZK 是强一致 CP(默认 leader 读)
[拼多多]R3 #6921 raft 算法基本流程?脑裂怎么处理?paxos 和 zab 区别?
答:Raft 任期 + 半数;ZAB 类似但偏广播;脑裂多数派胜出
[拼多多]R1 #1298 raft、paxos、zab 区别?
答:Paxos 通用复杂;Raft 易懂工程化;ZAB 为 ZK 优化的广播
[滴滴]R4 #4794 ZK 部署、ZAB 协议、ZXID 结构?
答:奇数节点;ZAB 选主 + 广播;ZXID 保证递增
[美团]R1 #4787 ZK 选举过程?ZAB 和 RAFT 最大区别?
答:ZAB 偏广播为高吞吐设计;Raft 偏日志复制为通用设计
[腾讯]R4 #3537 zk 原理(讲了 zab)
答:ZAB 协议 = 选主 + 广播;崩溃恢复保证已提交事务不丢
[蚂蚁]R1 #6667 zk 一致性原理
答:ZAB 半数 ack 写 + 强一致读 leader
distributed-lock-redis
🎯 骨架
基础命令:SET key value NX EX seconds,原子加锁 + 设过期时间
value 必须唯一(UUID/线程 ID),删锁前先比对,防止误删别人的锁
释放锁要原子:用 Lua 脚本做"比对 + 删除",否则比对后被抢占再删就误删
锁过期时间难定:业务执行超时则锁被自动释放,可能并发;解决靠 Redisson watchdog 自动续期
RedLock:跨多个独立 Redis 实例(≥5 个),半数以上加锁成功才算成功,应对单点故障
RedLock 争议:作者 Antirez vs Martin Kleppmann,时钟漂移和异步复制下不安全;实战慎用
Redis 主从复制非强一致:主挂从升级时锁可能丢失,CP 场景应用 ZK 锁
⚠️ 易混淆
SETNX + EXPIRE 两条命令不原子,必须用 SET NX EX 一条命令
删锁要原子(Lua),加锁也要原子(NX EX 一条命令)
watchdog 续期 ≠ RedLock,前者解决业务超时,后者解决主节点单点
AP 锁 vs CP 锁:Redis 默认 AP;ZK 是 CP
🎤 面经
[百度]R2 #7198 Redis 分布式锁如何保证原子性?
答:SET key val NX EX 一条命令;释放用 Lua 脚本
[腾讯]R1 #3108 如何防止 A 线程释放 B 的锁?
答:value 用唯一标识(UUID/线程 ID),释放前比对再删
[小米]R1 #3162 锁过期、锁误删、锁超时
答:value 唯一防误删;watchdog 续期防超时;锁过期靠 EX 设置
[携程]R1 #2896 分布式锁怎么实现?什么时候加?加在什么上?
答:SET NX EX;加在共享资源临界区;加细粒度(按 ID)
[快手]R2 #2807 MySQL 实现分布式锁?还有更好的方式?
答:MySQL 行锁压力大;Redis(高性能 AP)/ ZK(强一致 CP)更好
[B站]R1 #2757 项目中怎么实现分布式锁保证数据唯一?
答:Redis SET NX EX + Lua 释放 + Redisson 自动续期
[携程]R1 #2878 分布式锁讲讲
答:核心要素:互斥 + 防死锁 + 防误删 + 自动续期
[携程]R1 #3729 Redis 单线程分布式锁
答:单线程保证命令原子性,是 Redis 锁的基础
redisson-watchdog
🎯 骨架
解决问题:业务执行超时锁被自动释放,导致并发;watchdog 自动续期
默认:未指定 leaseTime 时启动 watchdog,每 10s 续期到 30s(lockWatchdogTimeout)
实现:Netty 时间轮(HashedWheelTimer)做定时调度,10s 一次发 PEXPIRE 命令续期
续期前提:客户端持有锁;客户端崩了/网络断了不再续期,锁到期自动释放
锁结构:底层 hash 结构,field=线程 ID,value=重入次数(支持可重入)
加锁/解锁/续期都用 Lua 脚本保证原子性
显式指定 leaseTime(如 lock(30, SECONDS))会关闭 watchdog;指定后到期就释放
⚠️ 易混淆
watchdog 是客户端定时器(Netty 时间轮),不是 Redis 端机制
watchdog 默认 30s + 10s 一续,不是固定值(lockWatchdogTimeout 可配)
watchdog 不解决主从切换丢锁,那要 RedLock
显式 leaseTime 关闭续期是常考点
🎤 面经
[美团]R1 #3096 Redisson 分布式锁源码?底层怎么实现?
答:SET NX EX + Lua + watchdog;hash 结构支持可重入
[滴滴]R3 #2986 Redission 底层实现原理?
答:Netty 时间轮 watchdog 续期;hash 重入;Lua 原子
[腾讯]R2 #2174 redis 分布式锁怎么用的(redission),原理知道吗?
答:核心:可重入 + 自动续期 + 原子加解锁
[Shopee]R3 #898 redission 怎么实现的?lua 脚本?
答:Lua 保证加锁原子;watchdog 定时续期;释放也用 Lua
[other]R1 #4466 Redisson 自动续期实现原理?watchdog 是咋实现的?
答:Netty 时间轮调度,10s 续到 30s;客户端崩则停止续期
[字节]R1 #979 Redisson 的时间轮了解过吗?说一下原理
答:HashedWheelTimer,O(1) 添加 + 槽位轮询;适合大量定时任务
[other]R1 #4426 Redisson watchdog 解决什么问题?拿到锁的机器挂了?
答:解决业务超时锁过期;机器挂了 watchdog 停,锁自然过期释放
[美团]R1 #4988 redission 实现分布式锁
答:底层 SET NX + 时间轮续期 + Lua 解锁
distributed-lock-zk
🎯 骨架
原理:客户端在锁目录下创建临时顺序节点,编号最小的获得锁
临时节点:客户端断连自动删除,避免死锁
顺序节点:唯一编号,最小者得锁
等待机制:非最小节点 watch 前一个节点的删除事件(避免羊群效应)
CP 强一致:基于 ZAB 协议,半数以上 ack 才提交,主从切换不丢数据
优势:可靠(CP)+ 自动释放 + 公平锁(按顺序)
劣势:性能差(写入要走 ZAB 半数 ack),并发上限低(千级),不适合高 QPS 场景
⚠️ 易混淆
ZK 锁是 CP,Redis 锁是 AP(默认配置)
watch 前一个节点 ≠ watch 父节点:watch 父节点会羊群效应(一释放全唤醒)
ZK 锁性能比 Redis 差 1-2 个数量级
临时节点过期是连接 session 超时(默认几十秒)
🎤 面经
[蚂蚁]R4 #3430 基于 redis 和 zk 的分布式锁分别有什么优缺点?
答:Redis 高性能 AP 可能丢锁;ZK 强一致 CP 性能低
[京东]R1 #4008 zookeeper 分布式锁实现原理?
答:临时顺序节点 + watch 前一个;最小者得锁
[蚂蚁]R1 #6667 zk 一致性原理
答:ZAB 协议 + 半数以上 ack;CP 强一致
[腾讯]R3 #3132 zookeeper 节点类型?选举机制?
答:持久/临时/顺序四种组合;选举基于 ZXID 大、myid 大
[蚂蚁]R1 #6834 项目中用到的中间件 Zookeeper
答:协调服务、配置中心、分布式锁、leader 选举
[字节]R1 #274 kafka 集群中 zookeeper 的作用?
答:存元数据 + controller 选举(Kafka 2.8 后可不依赖 ZK)
[Shopee]R1 #2782 raft 算法和 zk 选主算法
答:ZAB 类似 Raft(半数 ack + 任期),ZK 选主选 ZXID 大的
[携程]R1 #2888 zookeeper 的选举算法
答:FastLeaderElection;按 ZXID 和 myid 投票,半数以上当选
S3 分布式事务
2pc-protocol
🎯 骨架
2PC = Two-Phase Commit,强一致分布式事务协议,需要协调者 + 多个参与者
阶段 1(投票):协调者发 Prepare,参与者执行 SQL 但不提交,回 Yes/No
阶段 2(提交):所有 Yes → 协调者发 Commit;任一 No → 全部 Rollback
三大缺陷:① 同步阻塞(参与者锁资源直到协调者发指令)② 协调者单点(挂了卡死)③ 数据不一致(阶段 2 部分参与者收到 Commit 部分没收到)
3PC 改进:CanCommit + PreCommit + DoCommit,超时机制 + 协调者挂了参与者可独立提交
XA 是 2PC 的标准实现:MySQL/Oracle 都支持 XA 接口;Seata XA 模式直接用
实战很少用 2PC:性能差(锁占用长)、可用性差(协调者挂卡死),改用 TCC/Saga/消息
⚠️ 易混淆
2PC 的"两阶段" ≠ MySQL InnoDB 的 redo+binlog 两阶段提交(同名不同物)
XA = X/Open 提的分布式事务标准接口,2PC 是它的协议本质
3PC 解决了部分问题但没根治,工程上仍少用
强一致 ≠ 性能好,2PC 强一致代价是高延迟和低并发
🎤 面经
[美团]R1 #3093 2PC、3PC、Seata、TCC 这些方案的缺点?
答:2PC 阻塞 + 单点;3PC 加超时改协调者依赖;TCC 业务侵入;Seata 自动化
[蚂蚁]R1 #6694 二段、三段、TCC 优缺点
答:2PC 同步阻塞;3PC 引入超时;TCC 应用层补偿强一致
[蚂蚁]R1 #6717 两阶段提交、补偿事务、本地消息表、MQ 事务、Saga
答:5 种方案按强一致到最终一致排列,2PC 最强但最重
[蚂蚁]R4 #4185 2pc 大概过程?
答:阶段 1 投票(Prepare/Yes-No);阶段 2 决议(Commit/Rollback)
[蚂蚁]R1 #4105 分布式事务,2pc 与 3pc 区别,主要解决哪些问题?
答:3PC 加 CanCommit + 超时,缓解同步阻塞和协调者单点
[字节]R2 #467 2PC 的缺点,说一个解决办法?
答:阻塞 + 单点 + 不一致;改 TCC 或最终一致(消息+本地表)
[腾讯]R1 #5086 2pc、3pc、tcc、seata、本地事件表 + mq
答:选型按业务强弱一致诉求排(强一致 2PC,最终一致消息)
[other]R1 #4360 二阶段和三阶段提交区别?解决什么问题?
答:3PC 引入 CanCommit 探查 + 超时,缓解阻塞和协调者依赖
tcc-pattern
🎯 骨架
TCC = Try(资源预留)/ Confirm(确认提交)/ Cancel(释放预留),应用层补偿事务
Try 阶段:所有服务做资源检查 + 冻结(如冻结库存、预扣金额)
Confirm 阶段:所有 Try 成功 → 全部 Confirm 提交(幂等)
Cancel 阶段:任一 Try 失败 → 全部 Cancel 回滚(幂等)
三大问题:① 空回滚(Try 没执行就 Cancel,要识别)② 悬挂(Cancel 先到 Try 后到,要拒绝)③ 幂等(同一阶段被重复调用要安全)
解决三问题:状态表记录 XID + 业务 ID + 状态 + 时间,每个动作先查状态再操作
业务侵入大:每个接口要拆三个,对老业务改造成本高;适合金融/订单等强一致场景
⚠️ 易混淆
TCC ≠ 2PC:TCC 在应用层做(业务接口三阶段);2PC 在数据库层(XA 接口)
资源悬挂 = Cancel 先于 Try 到达;空回滚 = Try 没成功就被 Cancel
TCC 不解决"消息丢失",要靠事务消息或本地表保证
Confirm 和 Cancel 必须幂等,因为协调者可能重试
🎤 面经
[美团]R1 #3093 TCC 怎么做?有什么缺点?
答:Try-Confirm-Cancel 三阶段;缺点业务侵入大、要处理空回滚悬挂
[蚂蚁]R1 #6694 二段、三段、TCC 优缺点
答:TCC 在应用层补偿,强一致但代码量大
[蚂蚁]R1 #6800 阿里 dubbo、rocketmq 事务消息,TCC
答:TCC 是补偿型;事务消息是异步最终一致;按业务选
[京东]R1 #4007 tcc 为什么不可靠?
答:网络分区+宕机会导致悬挂/空回滚;要状态表+幂等才可靠
[腾讯]R1 #5086 tcc 资源悬挂问题
答:Cancel 先于 Try 到;Try 时先查 Cancel 标记,已 Cancel 直接退出
[other]R1 #4480 Seata TCC 和 AT 区别?空回滚悬挂怎么解决?
答:状态表 + 业务 ID + 时间戳,每步先查状态再执行
[other]R1 #4360 二阶段提交和 TCC 是一回事么?
答:不是。2PC 是数据库协议;TCC 是应用层三接口补偿
[蚂蚁]R1 #6717 5 种分布式事务方案中的 TCC
答:TCC 是补偿型,强一致但侵入大;适合金融场景
saga-pattern
🎯 骨架
Saga = 长事务拆成多个本地事务 + 每个事务有补偿动作(反向操作)
流程:T1 → T2 → T3 ... 失败时反向 C3 → C2 → C1 补偿
两种实现:编排式(Orchestration,中心协调器)/ 协同式(Choreography,事件驱动)
编排式:有一个 Saga 管理者按顺序调各服务(看得清逻辑,但有单点)
协同式:服务之间发事件触发,各自决策(去中心化,但流程难追踪)
不保证隔离性:中间状态可见(脏读),需要业务层加锁或语义补偿
适用场景:长事务(订单+库存+物流+发券),不要求强一致,能补偿即可
⚠️ 易混淆
Saga vs TCC:TCC 三阶段(Try/Confirm/Cancel),Saga 一阶段+补偿
Saga 补偿不是反向 SQL(如 update 回滚),是业务语义反向(取消订单/退款)
Saga 不保证 ACID 的 I(隔离性),看得到中间态
Seata Saga 模式适合长事务流程,业务接口已经存在的场景
🎤 面经
[蚂蚁]R1 #6717 Saga 事务模型
答:长事务拆本地事务 + 反向补偿;最终一致
[other]R1 #4470 Seata 各模式特点和适用场景?什么情况只能用 TCC 不能 AT?
答:Saga 长事务无 SQL 回滚;TCC 资源预留型;XA 强一致;AT 自动 SQL 回滚
[腾讯]R2 #3122 分布式事务的实现
答:方案有 2PC/TCC/Saga/事务消息;Saga 适合长流程
[美团]R1 #3093 分布式事务方案有哪些缺点?
答:Saga 不保证隔离性;不支持回滚到事务前
[other]R1 #4360 分布式系统的数据一致性如何保证?
答:Saga 是其中一种长事务+补偿的方案
[蚂蚁]R4 #4185 强一致和弱一致用什么方法?
答:强一致 2PC/TCC;最终一致 Saga/消息+本地表
[通用] Saga 编排式 vs 协同式区别?
答:编排式有中心协调器流程清晰;协同式事件驱动去中心化
[通用] Saga 怎么补偿不可逆操作(如发短信)?
答:发逆向通知 / 加判断条件防止重复 / 业务层避免不可逆步骤
message-transaction
🎯 骨架
解决场景:本地事务 + 发消息要原子性(订单创建后必须发消息扣库存)
RocketMQ 事务消息流程:发半消息 → 执行本地事务 → 提交/回滚半消息 → MQ 回查兜底
半消息(Half Message):消息已存到 MQ 但消费者不可见,等 commit 才可见
回查机制:本地事务结果未知(如发送方挂了)时 MQ 主动回查事务状态
本地消息表(替代方案):本地事务里同时插订单和消息记录,定时任务扫描发送
保证最终一致:消费者必须幂等,结合重试 + 死信队列处理失败
Kafka 事务消息:基于事务 ID + 协调者,保证 producer 写多个 topic/partition 的原子性
⚠️ 易混淆
RocketMQ 事务消息 ≠ Kafka 事务:RocketMQ 解决"发消息+本地事务"原子性;Kafka 解决"读-处理-写"端到端原子性
半消息阶段消费者收不到,提交后才投递
不保证消费成功,只保证投递成功;消费失败靠重试 + DLQ
本地消息表对 MQ 没要求,但要自己写定时任务和重试
🎤 面经
[阿里]R1 #88 消息队列事务消息的核心流程?怎么保证不丢不重?
答:半消息+本地事务+回查;不丢靠 ack+重试;不重靠消费者幂等
[字节]R3 #331 讲讲 RocketMQ 事务消息的发送过程
答:①发半消息 ②执行本地事务 ③commit/rollback ④回查兜底
[字节]R1 #450 RocketMQ 事务消息底层怎么实现的?可靠吗?
答:半消息 + 回查;可靠靠业务幂等 + 重试 + 死信队列
[快手]R1 #2633 不支持两段式提交怎么实现事务消息?
答:本地消息表方案:本地事务里插消息表 + 定时扫描发送
[Shopee]R1 #815 RocketMQ 事务消息:订单+发券如何最终一致?
答:发半消息 → 创订单 → commit;消费者幂等扣券;失败 DLQ 兜底
[拼多多]R1 #608 RocketMQ 事务消息
答:半消息 + 本地事务 + 回查;解决发消息和本地事务的原子性
[蚂蚁]R1 #6800 rocketmq 的事务消息,TCC
答:事务消息异步最终一致;TCC 同步强一致;按场景选
[蚂蚁]R1 #6717 MQ 事务消息、Saga 事务模型
答:MQ 事务消息是最常用的最终一致方案
S4 分布式 ID 与幂等
snowflake-algorithm
🎯 骨架
64 位结构:1 符号位 + 41 位时间戳 + 10 位机器 ID(5 DC + 5 Worker)+ 12 位序列号
41 位时间戳:毫秒级,从自定义 epoch 起约 69 年
10 位机器 ID:1024 个节点上限;5+5 拆分支持 32 机房 × 32 机器
12 位序列号:同毫秒内 4096 个 ID/节点
理论上限:1000ms × 4096 = 400 万 ID/秒/节点
时钟回拨问题:服务器时间倒退会生成重复 ID;解决:等待回拨时长 / 切换 workerID / 报错
美团 Leaf:Leaf-segment(号段)+ Leaf-snowflake(用 ZK 分配 workerID + 处理回拨)
⚠️ 易混淆
雪花算法生成 19 位数字,不是固定 19 位(可能 18 位)
时钟回拨重复风险来自时间戳部分相同 + workerID 相同
workerID 分配方案:手动配 / IP hash / ZK / 配置中心 / K8s StatefulSet 序号
不能直接用机器 IP 当 workerID,IP 段大于 1024
🎤 面经
[腾讯]R2 #3146 id 生成器怎么实现的,如何全局递增?
答:雪花算法(趋势递增不严格递增);全局严格递增用号段或 Redis INCR
[美团]R1 #4910 雪花算法。生成 ID 的逻辑?
答:64 位 = 时间戳 + 机器 ID + 序列号;保证趋势递增 + 全局唯一
[字节]R3 #397 雪花算法原理
答:64 位结构;时钟回拨是核心痛点;workerID 分配多方案
[网易]R1 #1608 雪花算法的原理
答:标准 64 位结构,重点讲时钟回拨和 workerID 分配
[字节]R2 #487 雪花算法;如何处理系统时间不一致?
答:等待回拨 / 切换 workerID / 拒绝服务三种策略
[other]R1 #4424 雪花算法(时间戳、机器码、业务类型、标记位),时钟回拨问题?
答:定制化结构常见;时钟回拨记录最大时间戳防回退
[other]R1 #4452 雪花算法多少位?一定 19 位吗?什么情况会重复?
答:通常 18-19 位;时钟回拨 + workerID 冲突会重复
[字节]R1 #4831 基于 k8s 部署的自动雪花算法机器中心
答:K8s StatefulSet 序号 = workerID,自动对应扩缩容
idempotency-design
🎯 骨架
幂等 = 同一操作多次执行结果一致;网络重试 / 消息重复 / 用户连点都需要幂等
接口幂等四方案:唯一索引 / 乐观锁版本号 / 业务状态机 / 防重 token(前端发起前申请)
唯一索引:DB 层兜底,重复插入抛异常被业务捕获,简单可靠
防重 token:前端先调 /token 接口拿 token → 提交订单带 token → 后端 Redis 原子 setnx 删除
状态机:订单 Pending→Paid→Shipped 单向流转,重复支付时检查状态拒绝
消息消费幂等:消费者侧建去重表(业务唯一键 → 已处理);或用 Redis SET NX 标记
HTTP 方法幂等性:GET/PUT/DELETE 幂等;POST/PATCH 不幂等
⚠️ 易混淆
幂等 ≠ 防重:幂等是结果一致(多次和一次效果同),防重是只执行一次
DELETE 幂等:删除一条已删除的资源仍然 OK(结果不变)
业务幂等 vs 数据幂等:前者操作语义层面,后者数据库层面
POST 协议层不幂等,但可通过业务设计(防重 token)变幂等
🎤 面经
[蚂蚁]R1 #6625 什么是幂等锁?幂等和锁的区别?业务幂等与数据幂等的区别?
答:幂等保证结果一致;锁保证互斥;业务幂等是接口语义,数据幂等是 DB 层
[滴滴]R3 #2989 RocketMQ 如何保障消息的幂等性?
答:消费者侧去重表 / Redis SET NX;用业务唯一键
[字节]R1 #4857 怎么解决网络波动导致的消息重复发送和重复消费?
答:发送方重试 + 消费方幂等(去重表 + 唯一键)
[阿里]R4 #147 Kafka 重复发了怎么办?
答:开启幂等 producer(PID + seq);消费侧再做业务幂等兜底
[B站]R1 #2758 怎么保证消息不会被重复发送?
答:发送方 enable.idempotence;消费方业务唯一键去重
[网易]R1 #1623 GET 一定幂等,POST 一定不幂等吗?
答:协议层 POST 不幂等;业务层可加防重 token 实现幂等
[网易]R1 #1624 用户创建订单点了 10 次,怎么保证只创建一个?
答:前端 token + 后端 Redis SET NX;或唯一索引(订单号唯一)
[携程]R3 #1990 消息队列应用(涉及幂等)
答:消费侧用业务唯一键 + 去重表;防止重复扣款/发券
S5 分布式治理
rate-limiting
🎯 骨架
四大算法:计数器(固定窗口)/ 滑动窗口 / 漏桶 / 令牌桶
固定窗口:每 1s 计数,简单但窗口边界会有 2 倍突发
滑动窗口:把窗口切片(如 100ms 一格),平滑边界突发
漏桶:请求排队按固定速率出桶,整形流量;不允许突发
令牌桶:固定速率往桶里放令牌,请求拿令牌才能过;允许短时突发(桶有容量)
漏桶 vs 令牌桶:漏桶严格匀速(保护下游);令牌桶允许突发(保护自己)
实现:Guava RateLimiter(令牌桶)/ Sentinel / Redis + Lua(分布式滑动窗口)
⚠️ 易混淆
漏桶和令牌桶的最大区别:能否应对突发流量
漏桶出口固定速率,输入超出就丢弃;令牌桶按速率发令牌,桶满则丢弃多余令牌
计数器固定窗口的"两倍突发":第 1s 末尾和第 2s 初连续放过 2 倍量
分布式限流要用 Redis + Lua 保证原子性,单机用 Guava RateLimiter
🎤 面经
[B站]R1 #5836 令牌桶为什么不用加锁?还知道哪些限流算法?
答:令牌桶用原子计数 / Redis 单线程;其他有滑动窗口、漏桶
[快手]R2 #6392 常见限流算法?令牌桶与漏桶区别?
答:4 种算法;漏桶严格匀速,令牌桶允许突发
[字节]R3 #989 实现简单令牌桶?加滑动版限制 1 小时不超 5 万?
答:标准令牌桶 + 滑动窗口结合;用 Redis + Lua 实现
[蚂蚁]R3 #4726 令牌桶真的能限流么?
答:能,但要结合实际 QPS 设速率和桶容量;分布式要用 Redis
[网易]R1 #1480 知道哪些限流算法?滑动窗口和令牌桶的区别?
答:滑动窗口是计数型整形;令牌桶是速率型允许突发
[other]R1 #4391 流量突增 100 倍如何解决?令牌桶为啥可以应对突发?
答:桶有容量积累令牌,突发可消耗已有令牌;漏桶不行
[B站]R1 #3313 如何实现一个限流器?
答:单机 Guava RateLimiter;分布式 Redis + Lua 滑动窗口
[字节]R2 #4872 限流怎么做的?说一说原理?
答:网关层 Sentinel + 业务层接口级限流;底层令牌桶或滑窗
MICROSERVICE
S1 单体到微服务
microservice-split
🎯 骨架
★ 核心方法论:DDD 战略设计——通过限界上下文(Bounded Context)确定服务边界
★ 三大评判维度:① 边界(模型生命周期是否一致)② 模型(高内聚低耦合)③ 团队(康威定律)
★ 五步拆分:梳理业务流程对齐通用语言 → 提取核心概念 → 理清概念关系 → 划分限界上下文 → 处理跨域诉求
拆分粒度:单一职责 + 高内聚低耦合;拆太细→链路长改动散;拆太粗→不灵活
哈啰拆法:商户管理域 + 资金结算域 + 商户经营域 三个限界上下文;7 个应用服务
通信选型:同步实时 → RPC;异步解耦 / 削峰 / 跨域通知 → MQ
拆分禁忌:过度拆分(链路长)/ 跨服务事务(用幂等+本地事务+MQ 最终一致)/ 跨库 JOIN(冗余或聚合服务)/ 共享数据库
⚠️ 易混淆
"限界上下文 ≈ 微服务",但不绝对(一个上下文可能拆多个服务,如读写分离)
DDD 不只是技术划分,更是和产品对齐通用语言
康威定律:服务边界对齐团队边界,避免跨团队频繁协调
🎤 面经
[蚂蚁]R4 #3435 微服务拆分的原则?了解 DDD 吗?你们的项目中有哪些领域模型?
答:DDD 限界上下文 + 三维度评判;项目领域模型有商户、资金账户、订单、清结算单
[字节]R1 #7324 微服务拆分的原则是什么?服务间通信用 RPC 还是消息队列,如何选择?
答:DDD 划界 + 单一职责 + 团队对齐;同步实时用 RPC,异步解耦用 MQ
[美团]R1 #1301 你们是如何进行微服务拆分的?
答:DDD 五步——通用语言 → 核心概念 → 关系 → 限界上下文 → 跨域诉求
[字节]R1 #614 描述微服务拆分原则,如何确定服务边界?
答:业务域 + 变更频率 + 团队结构 + 并发性能 四维度划分边界
[蚂蚁]R3 #无 微服务拆分的原则是什么?了解 DDD 吗?
答:限界上下文 + 三维度(边界/模型/团队)评判;DDD 是核心方法论
[字节]R3 #无 服务间通信用 RPC 还是消息队列,如何选择?
答:需要实时返回用 RPC;不需要立即结果或要削峰跨域用 MQ
[蚂蚁]R2 #无 你们的服务怎么拆分的?为什么要这样拆?
答:按 DDD 拆三个限界上下文 + 数据库按域独立 + 跨域用 ExternalDataProvider 隔离
S2 RPC 通信
rpc-principle
🎯 骨架
★ RPC = 让远程调用像本地方法一样透明:动态代理 → 序列化 → 网络传输 → 反序列化 → 反射调用 → 返回
★ 五大核心组件:动态代理(Stub)/ 序列化(Hessian/Protobuf)/ 网络传输(Netty 长连接 + 自定义协议)/ 服务发现 / 负载均衡
★ requestId 是异步多路复用核心:一条长连接同时跑几十个并发请求,IO 线程和业务线程靠它解耦
协议帧格式:[魔数 | 长度 | requestId | body],解码器靠长度解决粘包/半包
为什么 RPC 比 HTTP 快:二进制序列化更紧凑 / 长连接复用省握手 / 自带服务治理 / IDL 强类型
IO 线程只做编解码,业务逻辑提交线程池,保证 Netty 高吞吐不阻塞
哈啰 SOA:基于 gRPC + Protobuf + 加权随机负载均衡 + FailFast 容错 + 优雅停机权重 0→100 预热
⚠️ 易混淆
HTTP/2 已经支持二进制帧 + 多路复用,RPC 和 HTTP 性能差距在缩小
"为什么不用 HTTP"不是绝对的——gRPC 也基于 HTTP/2;核心差异在序列化和服务治理
长连接价值不是"省握手",而是"高频调用 + 多路复用"
🎤 面经
[网易]R1 #3285 RPC 原理。为什么会出现 RPC?RPC 的组成?RPC 的过程?
答:动态代理 → 序列化 → 网络传输 → 服务发现 → 负载均衡 → 反序列化 → 反射调用
[网易]R1 #3247 RPC 框架和普通 HTTP 有什么区别和优势?基于 TCP 封装还是 HTTP 封装的?
答:可基于 TCP 也可基于 HTTP/2;优势是二进制序列化 + 长连接 + 服务治理
[蚂蚁]R1 #6900 hsf 了解多少,原理?注册中心
答:阿里 HSF 类似 Dubbo,自定义协议 + 注册中心服务发现 + 客户端 LB
[网易]R1 #3248 rpc 是长连接吗?传输大文件还是基于流吗?
答:默认长连接复用;大文件用流式(gRPC Stream / HTTP/2 流式 body)
[网易]R1 #3287 如何进行序列化与反序列化?(Protobuf、Thrift、Avro)如何进行通信?
答:Protobuf TLV 编码 + 不存字段名 + Schema 演进;通信用 NIO/Netty 长连接
[蚂蚁]R1 #6695 RPC 过程
答:Consumer Stub 拦截 → 序列化 → Netty 发送 → Provider 反序列化 → 反射调用 → 返回
[拼多多]R3 #7028 链路追踪的信息是怎么传递的?RpcContext 是在什么维度传递的?
答:RpcContext 用 ThreadLocal 存 traceId/spanId;跨线程池用 InheritableThreadLocal 或 TTL
[滴滴]R1 #3028 rpc 中使用事务该咋办
答:跨服务事务用业务幂等 + 本地事务 + MQ 最终一致,或 Seata TCC
dubbo-architecture
🎯 骨架
★ 五大角色:Provider(服务提供者)/ Consumer(消费者)/ Registry(注册中心,常用 ZK/Nacos)/ Monitor(监控)/ Container(运行容器)
★ 调用流程:Provider 注册 → Consumer 订阅 + 本地缓存 → 代理拦截 → 序列化 → 长连接发出 → 反序列化 → 反射调用 → 返回
★ 协议层:默认 Dubbo 协议(Netty + Hessian2 序列化),支持 Triple(gRPC over HTTP/2)/ HTTP / RMI
负载均衡策略:Random(默认加权)/ RoundRobin(加权轮询)/ LeastActive(最少活跃)/ ConsistentHash(一致性哈希)
容错策略:Failover(默认重试)/ Failfast(快速失败)/ Failsafe(异常忽略)/ Failback(异步重试)/ Forking(并行调多个)
注册中心挂了仍能消费:Consumer 有本地缓存,已订阅服务列表可用,新服务无法发现
Dubbo 3.x 改用 Nacos + 应用级注册(vs 2.x 接口级),节省注册中心存储
⚠️ 易混淆
Dubbo 协议是 TCP 长连接 + Hessian2,不是 HTTP;Triple 才是 HTTP/2
客户端 Failover 重试只对幂等接口安全,写接口默认应该 Failfast
Monitor 不参与调用链路,只做指标统计
🎤 面经
[蚂蚁]R1 #6803 Dubbo 踩过哪些坑,分别是怎么解决的?
答:常见坑——超时设置不合理、序列化兼容、注册中心抖动;解法靠超时分级 + 兼容序列化 + 本地缓存兜底
[滴滴]R1 #3023 dubbo 底层协议
答:默认 Dubbo 协议(TCP + Netty + Hessian2),自定义魔数/长度/requestId/body 帧格式
[百度]R2 #7203 dubbo 有哪几种负载均衡策略?
答:Random(加权随机,默认)/ RoundRobin / LeastActive / ConsistentHash / 自定义
[蚂蚁]R1 #6800 中间件知道哪些,阿里的 dubbo,rocketmq 的事务消息,TCC
答:Dubbo 是 RPC 框架;事务消息用半消息 + Producer 二阶段确认;TCC 业务级 try-confirm-cancel
[携程]R1 #2886 dubbox 如何运行
答:dubbox 是当当扩展版,加了 RESTful 支持;运行流程同 Dubbo
[蚂蚁]R4 #3431 我看你对 dubbo 很了解,可以详细说说嘛
答:从五大角色 + 调用流程 + 协议 + 负载均衡 + 容错 + 注册中心 6 个维度展开
[京东]R1 #3061 dubbo 调用流程,注册中心挂了还能消费吗
答:能;本地缓存已有服务列表,可调用已知节点,仅无法发现新服务
[百度]R1 #2859 Dubbo 的底层实现原理和机制 描述一个服务从发布到被消费的详细过程
答:注册 → 订阅 → 代理拦截 → 序列化 → 长连接 → 反射调用;细节按五大角色展开
feign-vs-dubbo
🎯 骨架
★ Feign:基于 HTTP/1.1 + JSON,声明式接口;底层 HttpClient + Ribbon
★ Dubbo:基于 TCP 自定义协议 + Hessian2,二进制序列化 + 长连接
★ 性能:Dubbo 更快(二进制 + 长连接 + 多路复用);Feign 通用性更好(HTTP 可调任意服务)
生态:Feign 在 Spring Cloud 体系;Dubbo 是独立框架,3.x 集成 Spring Cloud
服务治理:Dubbo 内置丰富(路由、容错、负载均衡);Feign 靠 Spring Cloud 组件拼装(Ribbon+Hystrix+Sentinel)
HTTP/2 + Protobuf 缩小性能差距:gRPC 性能接近 Dubbo,且跨语言更好
哈啰自研 SOA 基于 gRPC + Protobuf + 加权随机 LB,原理类似 Dubbo
⚠️ 易混淆
"RPC 比 HTTP 快"是相对的——HTTP/2 + 二进制序列化(gRPC)也很快
Feign 不是真 RPC,是 HTTP 客户端封装;本质走 HTTP/JSON
Dubbo 不只 Java——支持多语言(Triple 协议)
🎤 面经
[字节]R2 #670 为什么使用 feign 代替了 dubbo 呢?feign 调用和 dubbo 有什么不同?
答:Feign 通用性好(HTTP)跨语言,Dubbo 性能更好(二进制+长连接);选型看团队栈和性能要求
[字节]R2 #670 RPC 自定义协议和 HTTP 有什么不同?
答:自定义协议头紧凑、二进制序列化、长连接复用;HTTP 头大、文本、HTTP/1.1 队头阻塞
[other]R1 #4442 Dubbo 和 feign 有啥区别?rpc vs http,为啥 rpc 快?
答:协议紧凑 + 序列化高效 + 长连接复用;Ribbon 是客户端 LB,从本地服务列表选节点
[Shopee]R1 #811 Feign 性能调优:如何替换默认 URLConnection 为 OkHttp?配置连接池参数
答:feign.client.config 加 OkHttp 依赖,配置 maxConnections/timeout 提高吞吐
[小米]R3 #7222 feign 里集成 hystrix 熔断和降级
答:Feign + Hystrix 组合:Feign 接口加 fallback 类;下游挂了走 fallback 兜底
[字节]R1 #260 Spring AOP 的原理 JDK、CGLIB 实现的区别(关联 Feign 接口代理)
答:Feign 接口靠 JDK 动态代理生成 Stub,调用时序列化 HTTP 请求发出
[蚂蚁]R1 #6803 Dubbo 踩过哪些坑(关联 Dubbo 性能 vs Feign)
答:Dubbo 长连接 + 二进制比 Feign 短连接 + JSON 性能高 5-10 倍
S3 服务发现与注册
service-discovery
🎯 骨架
★ 服务发现 = Provider 启动时把 IP+端口注册到注册中心,Consumer 订阅 + 本地缓存可用节点列表
★ 三种注册中心:ZK(CP)/ Nacos 临时实例(AP,Distro)/ Eureka(AP,已停维护)
★ 推/拉模型:Consumer 启动时全量拉,注册中心 Watch 推送变更(ZK Watch / Nacos UDP / Eureka 增量拉)
注册中心挂了仍能调用:Consumer 有本地缓存,短期内正常,只是无法感知新上下线节点
心跳机制:Provider 定期续约,超时自动摘除(ZK 临时节点 30s 会话;Nacos 5s 心跳,15s 不健康,30s 摘除)
优雅停机:权重调 0 → Watch 通知 Consumer → 等存量请求处理完 → 关闭进程
哈啰用 ZK + Apollo:ZK 注册临时节点路径 /soa/服务名/providers/IP:Port
⚠️ 易混淆
服务发现是"找到能调的节点",不是"找到调用结果"——后者在 RPC 层
客户端发现(Consumer 本地选)vs 服务端发现(中间代理选):Spring Cloud 走客户端,K8s Service 走服务端
Nacos 临时实例 AP,持久实例 CP,按场景选(无状态用临时,有状态用持久)
🎤 面经
[腾讯]R2 #3142 微服务的特点,如何实现服务发现和负载均衡
答:Provider 注册到注册中心,Consumer 订阅本地缓存;负载均衡客户端从缓存选节点
[网易]R1 #3286 如何做到透明化远程服务调用?如何进行服务发布?(ZooKeeper)
答:动态代理 + 注册中心 + 序列化协议;ZK 创建临时节点发布服务
[Shopee]R1 #6436 Dubbo 的服务发现是怎么实现的?
答:Provider 启动注册到 ZK;Consumer 订阅服务目录,节点变更靠 Watch 推送
[Shopee]R1 #904 Dubbo 包括哪些组件? Dubbo 的服务发现是怎么实现的?
答:Provider/Consumer/Registry/Monitor;服务发现走注册中心订阅 + 本地缓存
[阿里]R4 #186 谈谈 consul 和 zookeeper 服务发现机制
答:Consul 用 Gossip + Raft,自带健康检查;ZK 用 ZAB + 临时节点心跳
[阿里]R5 #227 zookeeper 是怎么服务发现的,zookeeper 心跳检测
答:临时节点存 IP+端口;Session 心跳维持节点,超时自动删除
[字节]R2 #507 consul 的原理有了解过么?服务发现、健康检查、多数据中心
答:Gossip 协议传播节点状态;TTL 或 HTTP/TCP 探测健康;多数据中心走 WAN Gossip
[滴滴]R3 #2284 详细解释微服务;如何实现服务发现与注册;如果某个服务挂掉,如何通知调用者
答:注册中心维护节点列表;服务挂了 Watch 推送变更到 Consumer,本地缓存自动剔除
nacos-vs-eureka-vs-zk
🎯 骨架
★ ZK = CP(ZAB),Eureka = AP,Nacos = AP+CP 双模式(临时实例 Distro AP / 持久实例 Raft CP)
★ ZK 的 Watch 是一次性的(避免处理中间态),需重新注册;Nacos 用 gRPC 长连接持续推送
★ Etcd(K8s 用)= CP(Raft),revision 机制不漏事件,比 ZK 一次性 Watch 更可靠
ZK 适合分布式协调(锁、Leader 选举),不为服务注册发现设计;脑裂时少数派停服
Eureka 客户端拉取 + 自我保护机制(连续失败不立即摘除节点);已停维护
Nacos 选 AP 临时实例的原因:服务注册"宁可短暂不一致,不能不可用"
哈啰场景:ZK 做注册中心 + Apollo 做配置中心;Nacos 集团内 hammer-plus 线程池在用
⚠️ 易混淆
ZK 选 CP 不是"必须"——是它的设计目标(分布式协调),用来做注册中心其实是错配
"Eureka 已停维护"指 Netflix Eureka 2.x,1.x 仍可用但不推荐
Nacos 临时/持久实例不是 AP/CP 二选一固定——不同场景用不同实例类型
🎤 面经
[字节]R1 #472 说说自己做的项目为啥使用 nacos 作为注册中心;nacos 和 etcd 注册中心的区别
答:Nacos 临时实例 AP 适合服务注册;Etcd Raft CP 适合 K8s 元数据,不漏事件
[京东]R2 #5952 Nacos 实现的原理?监听器如何和 Nacos 通信?
答:临时实例 Distro 协议;客户端用长轮询/gRPC 长连接,配置变更立即推送
[小红书]R1 #5288 Nacos 实现原理?还了解其他的注册中心/配置中心框架吗?
答:临时 AP(Distro)/ 持久 CP(Raft);其他有 Eureka/ZK/Consul/Apollo
[京东]R1 #5937 zk/nacos 比较
答:ZK CP 强一致但分区不可用;Nacos AP 高可用,配置中心有 UI 和命名空间隔离
[other]R1 #4455 Nacos 是 AP 还是 CP?配置中心是哪种?为啥配置中心 C 比 A 更重要?
答:Nacos 双模式;配置中心倾向 CP——两份不同配置比短暂不可用更危险
[网易]R1 #1597 Nacos 健康检查机制和高可用性如何保证?
答:临时实例靠客户端心跳(5s/15s/30s),持久实例靠服务端探测;集群 Distro 数据均摊
[字节]R1 #459 eureka 原理,强一致性么,多级缓存,宕机了服务还能调用么
答:Eureka AP 不强一致;三级缓存(registry/readWrite/readOnly);宕机 Consumer 本地缓存可用
[快手]R2 #6323 简单介绍一下 eureka;eureka 服务器集群状态下如何进行相互的数据交互
答:Eureka 节点互为 Peer,注册/续约/下线事件互相同步,最终一致
S4 熔断限流降级
circuit-breaker
🎯 骨架
★ 熔断核心目的:保护自己(主链路),下游慢了线程阻塞会被拖垮
★ 三态机:CLOSED(正常)→ 异常率超阈值 → OPEN(快速失败)→ 等恢复时间 → HALF_OPEN(CAS 放一个试探)→ 成功 CLOSED / 失败 OPEN
★ 触发条件:异常比例(如 > 50%)+ 最小请求数(如 ≥ 20),双条件防误判(单次失败不熔断)
三种熔断策略:异常比例 / 异常数 / 慢调用比例(RT > 阈值占比)
HALF_OPEN 只放一个请求:CAS 从 OPEN→HALF_OPEN,只一个线程能成功
限流 vs 熔断 vs 降级:限流保护自己(入口)/ 熔断保护自己(出口下游异常)/ 降级是失败后处理策略
哈啰:FailFast + 降级兜底,FailSafeFilter 永远不抛裸异常,包装为 ErrorResponse + Mock
⚠️ 易混淆
熔断 ≠ "给下游恢复时间"——核心是保护自己,下游恢复是副作用
HALF_OPEN 不是"放多个请求测试",是 CAS 抢占只放一个
降级不是独立触发机制,是限流/熔断/超时之后的处理策略
🎤 面经
[腾讯]R2 #3145 熔断是怎么实现的
答:滑动窗口统计异常率,超阈值打开断路器;HALF_OPEN 试探后决定恢复
[字节]R1 #460 hystrix 原理,半开状态知道么,具体的转换过程,隔离怎么实现的
答:CLOSED→OPEN→HALF_OPEN 三态机;Hystrix 用线程池隔离,每依赖一个池
[蚂蚁]R1 #6706 从 hystrix 一路问到原理->自己如何实现->如何优化->响应流编程
答:滑动窗口 + 状态机 + 信号量/线程池隔离;优化点是规则动态化 + 热点参数限流
[携程]R1 #7250 大量重试导致下游服务压力升级,有哪些手段可以预防
答:消费端限流 + 熔断 + 死信队列 + 指数退避重试 + 流控开关
[字节]R2 #4870 熔断机制,怎么实现?高可用怎么保证?
答:滑动窗口 + 三态机 + CAS 半开试探;高可用从减少故障 + 快速恢复两端发力
[美团]R3 #1226 熔断机制,怎么实现
答:阈值触发开断路器,快速失败保护主链路;HALF_OPEN 试探后决定关闭
[京东]R3 #2067 你们的熔断降级怎么做的?
答:项目用 Sentinel 内置三层 Entry(服务/接口/方法);FailSafeFilter 兜底返回 Mock
[小米]R3 #7222 提供服务者挂了怎么办(feign 集成 hystrix 熔断和降级)
答:Feign + Hystrix fallback;熔断打开后直接走 fallback,不再压下游
hystrix-vs-sentinel
🎯 骨架
★ Hystrix:Netflix 出品,已停维护;线程池/信号量隔离 + 熔断 + 降级
★ Sentinel:阿里出品,活跃维护;信号量隔离 + 熔断 + 限流(QPS/线程/热点)+ 系统自适应
★ Sentinel 比 Hystrix 多:QPS 限流 / 热点参数限流 / 系统规则 / Dashboard 实时监控 / 规则动态推送
隔离方式:Hystrix 默认线程池隔离(资源开销大但能超时取消);Sentinel 默认信号量隔离(轻量但无法超时)
滑动窗口实现:Sentinel 用 LeapArray 环形数组,每格记录请求数+异常数+RT
熔断策略:Hystrix 只异常比例;Sentinel 三种(异常比例/异常数/慢调用比例)
哈啰场景:SOA 框架内置 Sentinel 三层 Entry(服务/接口/方法),Apollo 推规则
⚠️ 易混淆
Sentinel 默认信号量隔离不是"无超时"——配合 Sentinel 的 RT 阈值熔断兜底
Hystrix 线程池隔离能限定每依赖资源占用,但线程切换开销大
限流和熔断是不同维度——前者控入口流量,后者控对下游调用
🎤 面经
[字节]R1 #460 hystrix 原理,半开状态知道么,具体的转换过程,隔离怎么实现的
答:CLOSED→OPEN→HALF_OPEN;隔离用线程池或信号量;半开 CAS 放一个试探
[蚂蚁]R1 #6706 从 hystrix 一路问到原理->自己如何实现->如何优化->响应流编程
答:滑动窗口 + 状态机 + 线程池隔离;优化点是规则动态化(Sentinel 路线)
[蚂蚁]R4 #4160 限流算法有哪些?中心化限流是怎么做的?Sentinel 的原理是什么?
答:滑动窗口(LeapArray)+ 滑动计数;中心化用 Token Server(独立 Java 进程)
[滴滴]R2 #无 熔断限流怎么做的?Sentinel 怎么实现的?
答:项目用 Sentinel 三层 Entry,每层独立配规则;Apollo 推规则秒级生效
[网易]R1 #1588 基于 Guava RateLimiter 或 Sentinel 如何实现高并发下的接口限流?
答:Guava 令牌桶单机;Sentinel 滑动窗口 + 集群 Token Server;选哪个看是否要分布式
[腾讯]R2 #3145 熔断是怎么实现的
答:状态机 + 滑动窗口统计;Sentinel/Hystrix 都基于这套
[字节]R2 #4870 熔断机制怎么实现
答:异常率超阈值打开断路器;HALF_OPEN CAS 放一个试探;Sentinel 比 Hystrix 多 RT 慢调用熔断
[小米]R3 #7222 提供服务者挂了怎么办(hystrix 熔断和降级)
答:Hystrix 走 fallback,Sentinel 走 BlockHandler;返回降级数据保护主链路
rate-limiting-strategy
🎯 骨架
★ 五大算法:固定窗口 / 滑动窗口(格子或 ZSet)/ 令牌桶(Guava)/ 漏桶
★ 固定窗口边界突刺 2×limit;滑动窗口(格子)突刺 1+1/(N-1);ZSet 精确无突刺;漏桶绝对匀速;令牌桶允许突发
★ 令牌桶 vs 漏桶:令牌桶允许突发(积令牌);漏桶严格匀速(恒定出口速率)
单机限流(Sentinel 滑动窗口)vs 分布式限流(Redis Lua / Sentinel 集群 Token Server)
acquire() 是整形(阻塞等待,所有请求最终通过);tryAcquire() 才是真限流(拒绝多余请求)
SmoothWarmingUp 冷启动保护:令牌多时取令牌代价高(防冷启动被打满)
哈啰场景:外部用网关分布式限流,内部走 Sentinel 单机限流(负载均衡均匀分散)
⚠️ 易混淆
"限流 = 拒绝"是错的——令牌桶 acquire() 是排队等待
滑动窗口的"格子数"= 精度,不是窗口大小;总窗口固定,格子越多突刺越小
Redis ZSet 限流精确但内存大(每请求一条记录),高 QPS 慎用
🎤 面经
[携程]R1 #2914 限流算法
答:固定窗口/滑动窗口/令牌桶/漏桶;选型看突刺容忍度和分布式需求
[B站]R1 #5836 令牌桶为什么不用加锁而用令牌桶?你还知道哪些限流算法?
答:令牌桶用 CAS 或纳秒级 synchronized 极短临界区;其他算法见上
[蚂蚁]R1 #6732 如何实现高并发下的削峰,限流?
答:MQ 削峰 + 令牌桶限流 + 熔断兜底;分布式用 Redis Lua 或 Sentinel 集群
[蚂蚁]R2 #4116 限流原理,有无读/写过相关实现
答:滑动窗口 LeapArray 环形数组;每次请求驱动统计,无后台线程
[小红书]R1 #4070 设计一个单机限流组件能够在多线程环境下安全运行,并能根据下游请求的错误率对限流阈值进行动态自适应调整
答:分段锁 + AtomicLong 减少竞争;错误率反馈调整阈值(PID 或简单线性回归)
[字节]R2 #4872 你们限流怎么做的?说一说原理?
答:Sentinel 滑动窗口三层 Entry;规则用 Apollo 推送,秒级生效
[百度]R1 #无 Redis 限流器可以怎么设计?比如限流一分钟 20 次
答:ZSet score=时间戳;Lua 原子执行 ZREMRANGEBYSCORE + ZCARD + ZADD
[字节]R2 #无 设计一个分布式限流系统,支持每秒 10 万 QPS
答:Sentinel 集群 Token Server(独立 Java 进程,内网 TCP < 0.5ms);挂了降级单机
S5 服务治理与部署
docker-basics
🎯 骨架
★ 容器 vs 虚拟机:容器共享宿主机内核,进程级隔离(轻量启动快);VM 有独立内核,硬件级隔离(重)
★ 隔离技术:namespace(资源视图隔离 PID/Net/Mount/UTS/IPC/User)+ cgroup(资源用量限制 CPU/内存/IO)
★ Image / Layer / Container:Image 是只读模板(多层叠加);Layer 用 UnionFS 分层;Container 是 Image + 可写层
写时复制(COW):Container 启动时不复制 Image,只在写入时把对应文件复制到可写层
Dockerfile 关键字:FROM(基础镜像)/ RUN(构建时执行)/ COPY(文件复制)/ EXPOSE(端口)/ CMD(启动命令)
K8s 核心:Pod(最小调度单元)/ Service(虚拟 IP + LB)/ Deployment(副本管理)/ Scheduler(节点调度)
哈啰场景:服务部署在 K8s + Docker,CI/CD 自动构建镜像 + 灰度发布
⚠️ 易混淆
容器不是"轻量级 VM"——核心区别是是否共享内核
Docker 不等于容器,是容器引擎之一(还有 containerd / podman)
K8s 不依赖 Docker,可换 containerd(K8s 1.20+ 默认 containerd)
🎤 面经
[B站]R1 #5840 docker 部署的流程
答:写 Dockerfile → docker build 镜像 → push 到 registry → 拉取镜像 → docker run 启动容器
[字节]R2 #508 docker 的 image、layer、container 分别讲下?Linux 中 namespace 底层数据结构
答:Image 只读模板,Layer 分层,Container = Image+可写层;namespace 在内核 task_struct 里维护
[百度]R1 #4016 docker 和 kubernetes 的原理和了解程度,docker 的 cgroup 了解么
答:Docker 用 namespace+cgroup 实现隔离和资源限制;K8s 调度容器编排
[百度]R2 #5666 谈谈你对 docker 的理解,Docker 和虚拟机有什么区别?Docker 底层是如何做资源隔离的?
答:进程级隔离 vs 硬件级隔离;namespace(视图)+ cgroup(用量)+ UnionFS(镜像分层)
[B站]R1 #5847 docker 容器是什么?docker 容器核心组件
答:基于 namespace+cgroup 的进程;核心组件 dockerd / containerd / runc / Image
[字节]R1 #617 解释容器化(如 Docker/K8s)在数据产品部署中的价值
答:环境一致性 + 快速弹性 + 资源隔离 + 自动恢复;解决"我机器上能跑你机器上跑不了"
[腾讯]R1 #2052 Docker 和虚拟机的区别;Docker 写时复制了解吗;K8s 的核心模块构成
答:进程级 vs 硬件级;COW 让 Container 启动只复制可写层;K8s 有 API Server/Scheduler/Controller/Etcd
SPRING
S1 控制反转的诞生
ioc-di-principle
🎯 骨架
★ IoC = 控制反转:对象创建权交给容器(不是 new 关键字),容器统一管理生命周期
★ DI = 依赖注入,是 IoC 的实现方式(构造器/Setter/字段注入)
★ 核心价值:解耦——业务代码只声明依赖接口,不关心实现来源/创建顺序
容器类型:BeanFactory(基础,懒加载)/ ApplicationContext(增强,预加载,事件/国际化)
Bean 注册三大方式:XML 配置 / @Component+ComponentScan / @Configuration+@Bean
依赖查找顺序:byType → 多个时 byName → @Qualifier 指定 → @Primary 兜底
三级缓存解决循环依赖:singletonFactories(工厂)→ earlySingletonObjects(半成品代理)→ singletonObjects(完整 Bean)
⚠️ 易混淆
IoC ≠ DI:IoC 是思想(控制反转),DI 是手段(依赖注入),还有一种实现叫依赖查找
@Autowired 默认 byType,@Resource 默认 byName(JDK 注解,不是 Spring 自带)
构造器注入 vs 字段注入:构造器注入推荐(不可变 + 显式依赖 + 单测友好),@Autowired 直接打字段是反模式
🎤 面经
[快手]R2 #6360 Spring 原理、Spring IOC、AOP
答:IoC 容器管理 Bean 生命周期,AOP 通过动态代理织入横切逻辑,两者是 Spring 核心
[百度]R1 #2861 IoC 容器的加载流程
答:refresh 12 步——读配置、注册 BeanDefinition、BeanFactoryPostProcessor、实例化单例 Bean
[网易]R3 #3187 Spring 具有什么特点(IOC 和 AOP)
答:IoC 控制反转解耦对象创建,AOP 切面编程统一处理横切关注点
[小米]R1 #3165 Spring 的核心思想是什么,依赖注入解决了什么问题
答:核心是 IoC + AOP;DI 解决"对象间硬编码 new 依赖"导致的紧耦合,让依赖关系交给容器管理
[网易]R1 #3215 知道 Spring 中的 IOC 和 AOP 吗?底层是如何实现的?
答:IoC 靠 BeanFactory + BeanDefinition + 反射创建;AOP 靠 JDK/CGLIB 动态代理 + ProxyFactory
[网易]R1 #1600 对 Spring 熟悉吗?聊聊 IoC 和 AOP?什么叫做 DI?
答:DI = 依赖注入,容器把依赖对象通过构造器/Setter/字段注入给目标 Bean
[京东]R2 #3065 Spring IoC AOP,实际项目中哪里用到了 AOP
答:IoC 管 Bean,AOP 用于事务/日志/限流/审计/参数校验等横切场景
bean-lifecycle
🎯 骨架
★ 四大阶段:实例化 → 初始化 → 使用中 → 销毁
★ 实例化 = 反射调构造函数,依赖此时全为 null
★ 初始化最复杂:属性填充 → Aware 回调 → BeanPostProcessor.before → @PostConstruct/InitializingBean → BeanPostProcessor.after
AOP 代理在 BeanPostProcessor.postProcessAfterInitialization 创建(Bean 完全装配后才包装代理)
@PostConstruct 在属性填充后执行,比构造函数晚——构造函数里 @Autowired 字段还是 null
销毁顺序:@PreDestroy → DisposableBean.destroy → destroy-method
多例 Bean(prototype)只管实例化和初始化,销毁交给业务代码
⚠️ 易混淆
BeanFactoryPostProcessor 处理 BeanDefinition(实例化前),BeanPostProcessor 处理 Bean 实例(实例化后)
@PostConstruct 是 JSR-250 规范注解,InitializingBean.afterPropertiesSet 是 Spring 接口,前者优先
prototype Bean 容器创建后不再管理,销毁需自己处理
🎤 面经
[百度]R1 #2862 Bean 的生命周期,需要说下涉及到的一些接口名
答:实例化→属性填充→Aware 回调→BeanPostProcessor.before→@PostConstruct/InitializingBean→after→使用→销毁
[美团]R1 #4894 bean 生命周期,加载 bean 之前要做一些前置操作怎么做
答:用 BeanFactoryPostProcessor(改 BeanDefinition)或 InstantiationAwareBeanPostProcessor.before
[携程]R1 #5249 bean 的生命周期,对象 new 和 bean 加载的顺序
答:refresh 阶段先注册 BeanDefinition,finishBeanFactoryInitialization 才批量 getBean 创建实例
[Shopee]R1 #5373 Spring bean 的创建过程,多例 Bean 什么时候创建?
答:单例容器启动时创建;多例 prototype 每次 getBean 才创建,容器不缓存
[京东]R1 #4004 Spring 中 bean 的生命周期?
答:实例化→属性填充→Aware→BPP.before→init→BPP.after(AOP 在这里)→使用→销毁
[小米]R1 #2123 Spring Bean 的生命周期是怎样的?
答:四大阶段,扩展点最多在初始化阶段,AOP 代理在 BPP.after 创建
[字节]R6 #393 spring 生命周期,几种 scope 区别
答:生命周期四阶段;scope 有 singleton/prototype/request/session/application
circular-dependency
🎯 骨架
★ 问题:A 依赖 B,B 依赖 A → 互相 new 无限递归 → StackOverflowError
★ 三级缓存:singletonFactories(工厂 Lambda)→ earlySingletonObjects(早期引用)→ singletonObjects(完整 Bean)
★ 核心思路:实例化后立即把"工厂"放三级缓存,被引用时才决定返回原始对象还是 AOP 代理
二级缓存不够:每个 Bean 都要提前判断是否需要 AOP 并创建代理,非循环依赖场景纯浪费
一级缓存不够:AOP 代理必须在初始化后创建,否则代理包装的是空壳
只能解决 setter/字段注入循环;构造器注入循环 Spring 解不了(实例化阶段就卡死)
prototype + 循环依赖 = 直接报错(prototype 不进缓存,每次都新建)
⚠️ 易混淆
三级缓存的"三级"是 singletonFactories(Lambda 工厂),不是某种"代理副本"
解决循环依赖 ≠ 鼓励循环依赖,循环依赖是设计 smell,正解是重构
@Async 方法 + 循环依赖容易出"BeanCurrentlyInCreationException"——@Async 会代理但代理时机晚
🎤 面经
[携程]R1 #2906 spring 三级缓存
答:一级存完整 Bean,二级存早期引用(AOP 代理),三级存工厂 Lambda;解决循环依赖时拿代理引用
[美团]R4 #1329 spring 循环依赖
答:三级缓存:实例化后放工厂,被引用时调 getObject 决定返代理还是原始对象
[滴滴]R3 #2598 spring 的循环依赖怎么解决,为什么需要三级缓存,二级不行么
答:二级缓存要每个 Bean 都提前创建代理浪费;三级用 Lambda 延迟,需要时才创建
[小米]R1 #3165 类 A 依赖 B,B 依赖 C,C 依赖 A 的时候怎么加载
答:A 实例化放三级缓存,填充 B → B 实例化填充 C → C 填充 A 时三级命中拿到 A 早期引用
[滴滴]R1 #2525 Spring 的循环依赖怎么解决的,为什么需要加个三级缓存,二级不行么?
答:核心是延迟创建代理;二级缓存要提前判断 AOP 创建代理浪费,三级 Lambda 只在循环时触发
[小米]R1 #2123 Spring 是如何解决循环依赖问题的?
答:三级缓存 + setter/字段注入;构造器注入循环解不了
[阿里]R1 #80 Spring 三级缓存分别存了什么?为什么用三级缓存而不是两级?
答:三级 Lambda 工厂、二级早期代理引用、一级完整 Bean;二级要提前创建代理浪费内存
[网易]R1 #1611 有几个类互相依赖,Spring 怎么管理哪个先 new 哪个后 new?
答:按 getBean 顺序触发,循环依赖靠三级缓存解决,构造器循环则报错
bean-scope
🎯 骨架
★ singleton(默认):容器唯一实例,启动时创建,容器关闭时销毁
★ prototype:每次 getBean 都创建新实例,容器不管理销毁
★ request / session / application:Web 场景,对应 HTTP 请求/会话/全局
WebSocket 场景额外有 websocket 作用域
singleton 是线程不安全的(实例共享),有状态字段需自己加锁或用 ThreadLocal
singleton 注入 prototype 默认拿到的是同一个 prototype(注入时机决定)→ 解决:@Lookup 或 ObjectFactory
⚠️ 易混淆
Spring 默认 singleton 是"容器级单例",不是 JVM 全局单例(不同容器各一份)
prototype 销毁交给调用方,容器只负责创建和初始化(不会调 @PreDestroy)
request/session 需要 web 容器,普通 Spring 容器没这俩
🎤 面经
[字节]R6 #393 spring 生命周期,几种 scope 区别
答:singleton 容器单例(默认);prototype 每次新建;request/session/application 是 Web 作用域
[Shopee]R1 #5373 Spring bean 的创建过程,多例 Bean 什么时候创建?
答:prototype 每次 getBean 才创建,容器只管实例化和初始化,销毁靠业务方
[小米]R1 #2123 Spring Bean 的生命周期是怎样的?
答:singleton 启动即创建走完整生命周期;prototype 每次新建,容器不管销毁
[百度]R1 #2862 Bean 的生命周期,需要说下涉及到的一些接口名
答:默认 singleton;prototype 销毁阶段不被容器管理
[京东]R1 #4004 spring 中 bean 的生命周期?
答:scope 决定生命周期长度,singleton 跟容器同生命周期,prototype 一次性
[京东]R4 #2142 解释一下 Spring Bean 的生命周期
答:scope 默认 singleton;prototype 容器只创建不销毁
autowired-injection
🎯 骨架
★ @Autowired 默认 byType,多个候选 byName,可加 @Qualifier 指定 / @Primary 兜底
★ @Resource(JSR-250)默认 byName,找不到 fallback byType;不是 Spring 注解
★ 三种注入方式:构造器(推荐,不可变 + 显式 + 单测友好)/ Setter / 字段
实现入口:AutowiredAnnotationBeanPostProcessor 在属性填充阶段反射设值
required = false 时找不到 Bean 不报错,注入 null(构造器注入除外)
注入集合:List 自动收集所有实现类,可配合 @Order 控制顺序
字段注入反模式:单测难(要反射 set)、final 不能用、循环依赖容易掩盖
⚠️ 易混淆
@Autowired 是 Spring 注解,@Resource 是 JDK(JSR-250)注解
@Inject(JSR-330)和 @Autowired 几乎等价,但不支持 required 属性
字段注入用 @Autowired 不会报"final 不能赋值"——AutowiredAnnotationBeanPostProcessor 反射改 final 字段
🎤 面经
[京东]R3 #3071 @Autowired 的实现原理
答:AutowiredAnnotationBeanPostProcessor 在属性填充阶段反射 set,先 byType 后 byName
[小米]R3 #7233 项目里用到了 SpringBoot 的 autowire 注解原理
答:本质是 BPP 在 populate 阶段反射注入,依赖查找走 DefaultListableBeanFactory.resolveDependency
[美团]R1 #4902 @Autowired 和 @Resource 区别
答:@Autowired 默认 byType(多个时 byName),Spring 注解;@Resource 默认 byName,JDK 注解
[美团]R4 #1334 Autowired 和 @Resource 区别
答:装配规则不同 + 来源不同;建议构造器 + @Autowired
[网易]R1 #1515 @Autowired 和 @Resource 的注解分别是什么,各自怎么用
答:@Autowired byType+@Qualifier;@Resource byName+name 属性
[小米]R1 #1505 @Autowired 和 @Resource 的注解分别是什么,各自怎么用
答:同上;推荐构造器注入避免字段注入反模式
[携程]R1 #1799 定义一个接口,有两个类实现这个接口,注入时怎么指定用哪个实现
答:用 @Qualifier("beanName") 或 @Primary 标记默认实现
[美团]R1 #1317 @Autowired 和 @Resource 这俩有什么区别?
答:注入策略+注解来源不同;@Resource 更接近 JEE,@Autowired 是 Spring 风格
S2 切面编程的力量
aop-principle
🎯 骨架
★ AOP = 面向切面编程,把横切逻辑(事务/日志/限流/权限)从业务代码抽离
★ Spring AOP 是运行时动态代理(不是编译期织入),AspectJ 才是编译期/类加载期字节码织入
★ 代理选型:有接口默认 JDK 动态代理,没接口或强制 proxy-target-class=true 用 CGLIB;Boot 2.x 默认 CGLIB
代理创建时机:Bean 初始化最后阶段 BeanPostProcessor.postProcessAfterInitialization(AnnotationAwareAspectJAutoProxyCreator)
五大通知:@Before / @After / @AfterReturning / @AfterThrowing / @Around(最强,能控制是否执行目标方法)
项目场景:事务 @Transactional、日志 @Around、限流 @SentinelResource、参数校验、多数据源切换、敏感字段脱敏
this 调用绕过代理 → AOP 失效(同类内部 this.method() 拿的是目标对象,不是代理)
⚠️ 易混淆
@EnableAspectJAutoProxy 借用 AspectJ 注解语法,底层仍是 Spring 运行时代理,不是 AspectJ 编译织入
private/final/static 方法 Spring AOP 拦不到(CGLIB 子类无法重写)
@Around 内必须 try/finally 调 pjp.proceed(),否则异常时埋点丢失
🎤 面经
[快手]R2 #6360 Spring 原理、Spring IOC、AOP
答:AOP 通过动态代理在 Bean 初始化后包装代理对象,切入横切逻辑
[快手]R2 #6464 AOP 的用法,用来做什么了(搜集日志)
答:把日志/事务/限流等横切关注点抽离到切面,业务代码只关注核心逻辑
[百度]R1 #2838 AOP 实现方式和背后原理?
答:JDK Proxy(接口)/ CGLIB(子类);运行时生成代理对象拦截方法调用执行通知链
[阿里]R4 #2956 Spring 的 AOP 的实现方法
答:BPP.postProcessAfterInitialization 检查切点匹配,匹配则用 ProxyFactory 创建 JDK/CGLIB 代理
[网易]R3 #3234 Spring IOC、AOP 原理及实现
答:AOP 用动态代理实现切面编程;JDK 基于反射 InvocationHandler,CGLIB 用 ASM 生成子类
[网易]R3 #3199 Spring 特性,IOC,AOP 原理及实现
答:同上
[网易]R1 #3215 Spring 中的 IOC 和 AOP 底层是如何实现的?
答:AOP 底层是 ProxyFactory + AdvisedSupport 组合,运行时生成代理对象
[京东]R2 #3065 Spring IoC AOP,实际项目中哪里用到了 AOP
答:事务/日志/限流/权限/审计/参数解析;项目 case 例如 @Transactional、@SentinelResource
jdk-vs-cglib-proxy
🎯 骨架
★ JDK 动态代理:基于接口,运行时 Proxy.newProxyInstance + InvocationHandler
★ CGLIB:基于继承,用 ASM 字节码生成目标类的子类
★ Spring 选型:有接口默认 JDK,无接口或 proxy-target-class=true 用 CGLIB;Boot 2.x 默认 CGLIB
JDK 代理只能按接口注入(生成的代理类只 implements 接口);CGLIB 可按实现类注入(子类 IS-A 父类)
CGLIB 限制:final 类无法继承、final/private 方法无法重写
性能:CGLIB 创建代理慢(生成字节码),调用快(FastClass 索引调用);JDK 创建快,调用走反射稍慢;JDK 8+ 后差距很小
Boot 默认 CGLIB 的原因:避免按实现类注入时的 ClassCastException
⚠️ 易混淆
"CGLIB 不能代理 final 类/方法"——指代理本身做不到,不是 Spring 限制
JDK 代理生成的类是 com.sun.proxy.Proxy0 这种命名,CGLIB 是 Target$EnhancerBySpringCGLIB$$xxx
CGLIB 也能代理实现了接口的类——它走的是继承不是实现,子类自然有接口能力
🎤 面经
[快手]R1 #2736 反射的优缺点和应用场景;JDK 动态代理和 CGLIB 的区别
答:JDK 基于接口反射,CGLIB 基于继承字节码;JDK 只代理接口,CGLIB 能代理普通类
[蚂蚁]R2 #4118 Java 动态代理
答:Proxy.newProxyInstance + InvocationHandler 拦截;只能代理接口
[字节]R1 #260 Spring AOP 的原理 JDK、CGLIB 实现的区别
答:JDK 反射调用接口方法,CGLIB 子类重写父类方法;选型按是否有接口
[拼多多]R3 #1279 AOP 的动态代理与 CGLIB 实现的区别。代理的实现原理呗
答:JDK 创建速度快但调用走反射;CGLIB 创建慢但 FastClass 索引调用快
[阿里]R5 #196 静态代理,动态代理
答:静态代理编译期写死代理类;动态代理运行时生成(JDK Proxy / CGLIB ASM)
[阿里]R4 #7092 JDK 动态代理和 CGLIB 的底层实现原理
答:JDK 用 Proxy + InvocationHandler;CGLIB 用 Enhancer + MethodInterceptor + ASM 生成子类
[美团]R1 #4993 Spring 动态代理是如何生成的,JDK 动态代理和 CGLIB 的区别
答:BPP.postProcessAfterInitialization 用 ProxyFactory 选 JDK/CGLIB;区别在接口/继承
[蚂蚁]R1 #4735 Spring 的 IOC 和 AOP?AOP 原理?没有用到 JDK 的动态代理吗?
答:用到,有接口走 JDK;Boot 2.x 默认 CGLIB 是为了避免类型转换异常
aspect-execution-order
🎯 骨架
★ 单切面五通知顺序:@Around 前 → @Before → 目标方法 → @AfterReturning/@AfterThrowing → @After → @Around 后
★ 抛异常时:@Around 后半段不执行,@AfterReturning 不执行,@AfterThrowing + @After 执行
★ 多切面排序:@Order/Ordered 接口控制;进入时 order 小先进,退出时 order 大先退(洋葱圈)
@Around 的 pjp.proceed() 把切面切成前后两段,前对应 @Before 时机,后对应 @AfterReturning 时机
同一切面内 @Around 包裹其他通知,所以 @Around 前 ≠ @Before(@Around 前比 @Before 更早)
生产写法:@Around 必须 try/finally 包 proceed,否则抛异常时埋点/耗时统计丢失
@Order 默认值是 Ordered.LOWEST_PRECEDENCE(最大 int,最后执行)
⚠️ 易混淆
@Around 前半段 ≠ @Before(同切面内 @Around 包 @Before)
多切面 @Order 排序遵循"洋葱圈"——order 1 包 order 2,order 2 包目标方法
@AfterReturning 和 @AfterThrowing 互斥:正常返回走前者,抛异常走后者
🎤 面经
[B站]R1 #5848 面向切面编程和面向对象编程的区别
答:OOP 纵向继承解决"一个对象多个能力";AOP 横向切片解决"多个对象共同行为"
[other]R1 #4383 AOP 切面实现多数据源切换介绍一下?
答:@Around 拦截 @DS 注解方法,前半段切换 ThreadLocal 数据源,目标方法执行后 finally 清理
[百度]R2 #6048 AOP 的原理,织入点和切面点代表什么意思
答:切点 Pointcut 表达式定义"在哪些方法织入";织入是把通知逻辑插到方法执行链
[快手]R2 #6360 Spring 原理、Spring IOC、AOP
答:通知链按 @Around 包裹 @Before → 目标 → @After 顺序执行;多切面靠 @Order 排序
[京东]R2 #3065 实际项目中哪里用到了 AOP?
答:事务、日志、限流、参数校验等横切场景;多切面同时存在时用 @Order 控制顺序
[阿里]R4 #2956 Spring 的 AOP 的实现方法
答:通知按链路顺序执行,@Around 最强能控制是否调用目标,多切面 @Order 决定嵌套顺序
[携程]R1 #2922 Spring AOP 原理
答:动态代理拦截 + 通知链顺序执行;@Around 包 @Before 包目标方法包 @After
S3 事务管理
transaction-propagation
🎯 骨架
★ REQUIRED(默认):有事务加入,没有就新建——大部分场景用这个
★ REQUIRES_NEW:挂起当前事务,无条件新建独立事务(新 Connection);适合审计/日志强隔离
★ NESTED:基于 savepoint 的嵌套事务(同 Connection);父回滚带子,子回滚不影响父
SUPPORTS(有就用没有就非事务)/ NOT_SUPPORTED(挂起当前以非事务执行)/ NEVER(有事务就抛错)/ MANDATORY(没事务就抛错)
REQUIRES_NEW 占用 2N 倍 Connection(父挂起不释放),大批量场景容易耗尽连接池
NESTED 用 savepoint 不耗连接池,但 JPA/Hibernate 不支持,多数公司规范禁用
this 调用绕过代理 → 传播行为不生效,必须跨 Bean 调用
⚠️ 易混淆
REQUIRED 是"复用调用链上已有事务",不是"继承父线程事务"(线程内传递)
REQUIRES_NEW 和 NESTED 区别:前者两个独立 Connection 同时存在,后者同 Connection + savepoint
父事务回滚:REQUIRES_NEW 子已 commit 撤不回;NESTED 子跟着回滚
🎤 面经
[百度]R1 #2839 Spring 有哪些事务传播机制?
答:7 种,常用 REQUIRED(默认)/REQUIRES_NEW(独立)/NESTED(savepoint 嵌套)
[Shopee]R1 #5371 Spring 事务的传播行为有哪些,传播行为实现机制
答:7 种通过 TransactionInterceptor 在切面里判断当前线程 ThreadLocal 是否有事务上下文
[腾讯]R5 #7089 Spring 事务传播机制
答:REQUIRED 加入或新建;REQUIRES_NEW 强制新事务;NESTED savepoint 嵌套;SUPPORTS/MANDATORY 等
[美团]R1 #1061 fun1 报错 fun2 不想一起回滚,有什么方法?
答:fun2 用 @Transactional(propagation=REQUIRES_NEW) 独立事务,必须跨 Bean 调用
[阿里]R5 #219 Spring 事务传播机制
答:核心三个 REQUIRED/REQUIRES_NEW/NESTED;其他 SUPPORTS/NEVER/NOT_SUPPORTED/MANDATORY
[Shopee]R1 #880 Spring 事务传递模式
答:靠 ThreadLocal 绑定 Connection,传播行为决定要不要复用、挂起或新建
[京东]R3 #5967 Spring 事务传播机制原理
答:基于 AOP,TransactionInterceptor 在 ThreadLocal 找当前事务,按传播行为决定操作
[B站]R3 #3317 Spring 事务注解原理,事务传播机制,使用过什么传播机制?
答:@Transactional 走 AOP;常用 REQUIRED;项目里审计日志用 REQUIRES_NEW
transaction-failure-scenarios
🎯 骨架
★ 三大类失效根因:① 不通过代理调用 ② 方法不可被代理 ③ 异常未传播到拦截器
★ this 调用(最高频):同类内 this.method() 拿目标对象,绕过代理 → 切面拦不到
★ private/final/static 方法:CGLIB 子类无法重写 → 代理拿不到调用
checked exception 默认不回滚(IOException、SQLException)→ 必须 rollbackFor=Exception.class
异常被 catch 吞掉不抛 → 拦截器看不到异常,正常 commit
多线程/异步:事务绑 ThreadLocal,新线程拿不到 Connection,独立事务
非 Spring Bean(new 出来的对象)→ 没经过代理,事务自然不生效
⚠️ 易混淆
@Transactional 加在 private 方法不报错但不生效(Spring 会扫到但拦不到)
final 方法 JDK 代理可以(实现类有非 final 接口方法),CGLIB 不行
内层 REQUIRED 抛异常被外层 catch → 外层 commit 时抛 UnexpectedRollbackException(rollbackOnly 标记)
🎤 面经
[快手]R2 #6387 Spring 事务什么时候会失效?
答:this 调用 / private 方法 / 异常被 catch / checked exception / 多线程 / 非 Spring Bean
[蚂蚁]R3 #4192 Spring 声明式事务哪些情况下会失效?
答:三大根因——不走代理、方法不可代理、异常没传播;最高频是 this 自调用
[other]R1 #4492 声明式事务事务失效了,可能有哪些情况?
答:private/this/AOP 没启用/final/rollbackFor 配错/数据库不支持/多线程
[other]R1 #4446 没走代理、异常处理、多线程事务能生效吗?
答:都不行;没走代理拦不到,异常 catch 吞了不回滚,多线程 ThreadLocal 拿不到事务
[京东]R1 #5919 声明式事务失效场景? 方法前加 final 会失效吗
答:失效,CGLIB 子类无法重写 final 方法
[小红书]R1 #5304 Spring 事务失效场景,优惠券服务中 @Transactional 为何不回滚?
答:常见是 this 调用或 catch 吞异常;可加 rollbackFor=Exception 或显式 setRollbackOnly
[网易]R1 #1595 @Transactional 失效的常见场景有哪些?如何排查?
答:this/private/final/异常被 catch/checked exception;排查看代理对象类名 + 异常路径
[B站]R1 #2755 怎么保证 Spring 事务不会失效?
答:跨 Bean 调用走代理 + rollbackFor=Exception.class + 不吞异常
transaction-rollback-rules
🎯 骨架
★ 默认只回滚 RuntimeException 和 Error,checked exception 默认不回滚
★ 设计初衷:checked exception = 可预期的业务异常(可恢复);RuntimeException = 不可预期 bug
★ 生产规范:所有 @Transactional 强制加 rollbackFor = Exception.class(阿里规范)
☆ 异常被 catch 吞掉 → 事务拦截器看不到异常 → 不回滚(最常见坑)
☆ 新线程中的异常不触发回滚 → 事务绑定 ThreadLocal,新线程拿不到当前 Connection
☆ BizException 继承 Exception 是 checked → 抛出后事务不回滚 → 数据不一致(高频生产坑)
☆ private/final 方法、this 调用 → AOP 代理拦截不到 → 事务不生效
☆ catch 后想手动回滚:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
☆ noRollbackFor:指定某些 RuntimeException 不回滚(如业务异常不需要回滚的场景)
☆ 给 fun2 加 @Transactional(propagation = REQUIRES_NEW) → 独立事务,fun1 回滚不影响 fun2
⚠️ 易混淆
@Transactional 加在 private 方法上不生效(CGLIB 无法重写 private 方法)
同类内部 this 调用绕过代理,事务不生效
🎤 面经
[网易][R3] 抛出异常后回滚规则?→ 默认只回滚 RuntimeException 和 Error,checked 不回滚
[网易][R3] 新线程中异常会触发回滚吗?→ 不会,事务绑定 ThreadLocal,新线程拿不到当前事务
[美团][R1] fun1 报错,fun2 不想一起回滚怎么做?→ fun2 加 REQUIRES_NEW 独立事务
[快手][R2] Spring 事务什么时候会失效?→ this 调用/private 方法/异常被 catch/checked 异常未配 rollbackFor/新线程
[B站][R1] 怎么保证 Spring 事务不失效?→ 走代理对象调用 + rollbackFor=Exception.class + 不吞异常
[滴滴][R1] Spring 事务失效的场景?→ 同类 this 调用、private 方法、异常被 catch、checked 异常
[百度][R2] MySQL 事务和 Spring 事务的关系?→ Spring 事务基于 AOP 封装 JDBC Connection,底层还是 MySQL 事务
S4 Boot 自动化革命
boot-auto-configuration
🎯 骨架
★ @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
★ SPI 入口:spring.factories(Boot 2.x)/ AutoConfiguration.imports(Boot 3.x),存配置类全限定名
★ AutoConfigurationImportSelector.selectImports 通过 SpringFactoriesLoader 拿到 200+ 配置类列表
@Conditional 家族过滤:@ConditionalOnClass(classpath 有)/ @ConditionalOnMissingBean(用户没自定义)/ @ConditionalOnProperty 等
关键设计 @ConditionalOnMissingBean:用户自定义 Bean 优先,框架默认让位(约定优于配置)
自定义 Starter 三步:autoconfigure 模块 + starter 空 pom + spring.factories 注册
Boot 启动流程:SpringApplication.run → prepareEnvironment → createApplicationContext → refresh → 自动配置在 invokeBeanFactoryPostProcessors 阶段触发
⚠️ 易混淆
"自动配置 = 全部加载"是错的,要过 @Conditional 过滤,最终生效的可能只有几十个
@SpringBootApplication 默认扫描当前包及子包,配置类放错位置可能扫不到
spring.factories 和 META-INF/spring/...AutoConfiguration.imports 是 Boot 2.x → 3.x 的迁移
🎤 面经
[小米]R1 #2124 Spring Boot 的自动配置原理是什么?
答:@EnableAutoConfiguration 通过 SPI 加载 spring.factories 配置类,@Conditional 过滤后注入容器
[阿里]R1 #81 SpringBoot Starter 自动配置的 SPI 机制核心是什么?怎么自定义一个 Starter?
答:SPI 文件 + 条件注解;自定义三步:autoconfigure 模块 + starter pom + SPI 注册
[Shopee]R1 #3789 springboot 做了哪些事,Starter 具体实现原理?
答:约定优于配置;Starter = autoconfigure + 条件注入 + spring.factories 声明
[小红书]R2 #7282 Spring Boot 中自动装配机制的原理?
答:@EnableAutoConfiguration 的 @Import 加载 ImportSelector,扫 SPI 文件 + 条件过滤
[京东]R1 #4047 Spring boot 启动流程,你知道 spring 里都用了哪些设计模式
答:启动走 SpringApplication.run;用了工厂、单例、模板方法、责任链、观察者
[网易]R1 #1590 Spring Boot 应用的启动性能优化有哪些优化手段?
答:减少 starter 数量、关 banner、懒加载 lazy-init、AOT 编译(GraalVM)
[百度]R2 #2103 让你设计一个 spring-boot-starter 你会怎么设计?
答:autoconfigure 模块写 @Configuration + @Conditional;空 starter 模块声明依赖;SPI 文件注册
[快手]R2 #6367 SpringBoot 启动过程
答:SpringApplication.run → 准备环境 → 创建上下文 → refresh(含自动配置)→ 发布 Started 事件
spring-mvc-flow
🎯 骨架
★ 入口 DispatcherServlet → HandlerMapping 找 Handler → HandlerAdapter 调 Controller → ViewResolver 解析视图
★ 完整 9 步:DispatcherServlet → HandlerMapping → Interceptor.preHandle → HandlerAdapter → Controller → Interceptor.postHandle → ViewResolver → View.render → afterCompletion
★ 设计模式:前端控制器(统一入口)+ 适配器(HandlerAdapter)+ 责任链(Interceptor)+ 策略(多种 HandlerMapping)
@RestController = @Controller + @ResponseBody,跳过 ViewResolver,直接走 HttpMessageConverter(Jackson)转 JSON
HandlerMapping 默认 RequestMappingHandlerMapping(处理 @RequestMapping);HandlerAdapter 默认 RequestMappingHandlerAdapter
异常统一处理:@ControllerAdvice + @ExceptionHandler,不是 AOP(是 MVC 内置机制)
哈啰场景:业务侧不直接用 MVC,对外 HTTP 走统一网关,内部走 SOA RPC
⚠️ 易混淆
@ControllerAdvice 不属于 AOP,是 DispatcherServlet 在 doDispatch 异常分支调用 ExceptionResolver
HandlerMapping 和 HandlerAdapter 不是同一个东西:前者找 Handler,后者负责调用
HttpMessageConverter(Jackson)做的是 body 序列化,和 ViewResolver(视图解析)不同
🎤 面经
[百度]R1 #2865 SpringMVC 的优势是啥,它的出现解决了一些什么问题?
答:前端控制器统一入口;解决了 Servlet 时代每个 URL 写 Servlet 类的繁琐
[蚂蚁]R1 #6801 讲一下 SpringMVC 的基本架构、请求流程
答:DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → ViewResolver
[滴滴]R4 #3050 讲一下 SpringMVC 的原理
答:注册 DispatcherServlet 把 request 按 URL 分发给不同 Controller 处理
[字节]R2 #323 SpringMVC 不同用户登录的信息怎么保证线程安全的?
答:登录态用 ThreadLocal(如 RequestContextHolder)或 HttpSession,请求级别隔离
[京东]R4 #1947 SpringMVC 优点,原理;AOP 优点,原理;IOC 优点,原理
答:MVC 前端控制器分层架构;AOP 横切解耦;IoC 反转控制
[网易]R1 #5552 Spring,SpringMVC 和 SpringBoot 的一个关系?
答:Spring 提供 IoC/AOP;SpringMVC 是 Web 模块;SpringBoot 是约定优于配置的脚手架
[得物]R1 #5832 SpringMVC 作用
答:基于 Servlet 封装,提供前端控制器 + 注解路由 + 数据绑定 + 视图渲染
S5 微服务与生态
spring-cloud-overview
🎯 骨架
★ 哈啰用自研 SOA + Apollo,不是 Spring Cloud 全家桶;面试答"原理熟悉,底层能力对标"
★ 六件套:注册发现(Eureka/Nacos)、RPC(Feign/Dubbo)、负载均衡(Ribbon/LoadBalancer)、熔断(Hystrix/Sentinel)、网关(Gateway/Zuul)、配置中心(Config/Nacos Config)
★ Nacos:临时实例 AP(Distro)/ 持久实例 CP(Raft),同时做注册中心和配置中心
Feign:声明式 HTTP,底层封装 HttpClient + Ribbon;Dubbo 是二进制 RPC 性能更好
Sentinel:QPS/线程数/热点参数限流 + 比例熔断;Hystrix 已停维护
Gateway:基于 Netty 异步,替代 Zuul 1.x 同步 Servlet;Predicate + Filter 链
Seata:AT(自动 undo log)/ TCC(业务级 try-confirm-cancel)/ Saga(长事务补偿)
⚠️ 易混淆
Netflix 栈(Eureka/Hystrix/Zuul/Ribbon)大部分已停维护,国内主流是 Alibaba 栈
客户端负载均衡(Ribbon)和服务端负载均衡(Nginx)不是一回事——前者从本地服务列表选实例
Spring Cloud 不是必须 Alibaba,可以混搭 Eureka + Feign + Sentinel
🎤 面经
[蚂蚁]R3 #4194 SpringCloud 知道吗?有哪些组件?每个组件的原理说一下?
答:Nacos 注册+配置;Feign 声明式调用;Ribbon 客户端 LB;Sentinel 熔断限流;Gateway 网关;Seata 事务
[百度]R2 #5646 springboot 和 springcloud 的区别,SpringCloud 能单独使用吗?
答:Boot 单服务脚手架,Cloud 微服务全家桶;Cloud 依赖 Boot,不能脱离
[蚂蚁]R3 #3887 谈谈 SpringBoot 和 SpringCloud 的理解
答:Boot 解决单体快速搭建;Cloud 解决多服务协同(注册/通信/熔断/网关/配置/事务)
[快手]R1 #2646 Spring Cloud 都包含哪些组件?
答:Eureka/Nacos、Feign、Ribbon、Hystrix/Sentinel、Gateway/Zuul、Config/Nacos Config、Seata
[蚂蚁]R1 #6729 Spring IOC、AOP?讲讲 SpringBoot/SpringCloud 的一些应用?
答:IoC/AOP 是 Spring 核心;Boot 简化配置;Cloud 提供微服务治理组件全家桶
[蚂蚁]R1 #6830 Spring IOC/AOP 相关知识。讲讲 SpringBoot 和 SpringCloud 的一些应用?
答:同上;项目中实际用 Boot + Apollo + 自研 SOA,对标 Cloud 能力
spring-project-experience
🎯 骨架
★ 框架栈:Spring Boot + 自研 SOA RPC(基于 gRPC)+ Apollo 配置中心 + 自研三网关
★ 核心使用点:IoC 管 Bean / AOP 处理事务+限流+审计 / @Transactional 声明式事务 / Apollo 动态配置
★ 与 Spring Cloud 对照:SoaServer/Client ≈ Nacos+Feign+Ribbon 三合一
AOP 落地:@Transactional 事务 / @SentinelResource 限流 / @DS 多数据源 / 参数解析切面 / 全局异常拦截
配置中心:Apollo 长轮询,开关秒级生效;项目里 22 个稳定性预案都靠 Apollo 触发
启动流程踩坑:@Async + @Transactional 不能同方法 / 静态变量引用配置中心拿不到运行时值
性能优化:lazy-init / @ComponentScan 收窄范围 / starter 精简
⚠️ 易混淆
自研 SOA 不等于不懂 Spring Cloud,原理是通的
@Transactional 在哈啰主要管业务幂等的写库流程,跨服务事务走 HMS 消息队列最终一致
Apollo 是携程开源,不是 Spring 官方组件,但和 @Value/@RefreshScope 集成顺滑
🎤 面经
[快手]R2 #6360 Spring 原理、Spring IOC、AOP
答:IoC 容器管 Bean;AOP 用动态代理织入横切;项目用 @Transactional/@SentinelResource 等切面
[网易]R1 #3215 知道 Spring 中的 IOC 和 AOP 吗?在项目中哪些地方体现?
答:所有业务 Bean 由 IoC 管理;AOP 落事务/限流/审计/参数校验/多数据源切换
[京东]R2 #3065 Spring IoC AOP,实际项目中哪里用到了 AOP
答:事务 @Transactional、限流 @SentinelResource、统一日志 @Around、敏感字段脱敏切面
[蚂蚁]R1 #6729 Spring IOC、AOP?讲讲 SpringBoot/SpringCloud 的一些应用?
答:项目 Spring Boot + 自研 SOA + Apollo;IoC/AOP 落地事务/限流/统一异常
[快手]R2 #6464 AOP 的用法,用来做什么了(搜集日志)
答:日志采集、链路追踪、限流、参数校验、多数据源等横切场景统一切面处理
[携程]R1 #2911 springboot
答:项目用 Boot 做脚手架,Apollo 替代 Nacos Config,自研 SOA 替代 Feign+Ribbon
[蚂蚁]R1 #6830 Spring IOC/AOP 相关知识。讲讲 SpringBoot 和 SpringCloud 的一些应用?
答:项目里 Boot + 自研 SOA 全套,对标 Cloud 能力;面试可主动讲哈啰为什么自研
Collect
Get Started
Collect
Get Started
Collect
Get Started
Collect
Get Started
评论
0 条评论
下一页