数据库内核复习大纲:索引、事务、恢复与分布式¶
约 4920 个字 1 张图片 预计阅读时间 25 分钟
使用方式¶
这篇文档不追求把所有知识点讲成教材,而是服务于数据库内核 / 存储引擎 / 分布式数据库面试复习。内容来源有三部分:
- CMU 15-445 / bustub 的课程与项目知识框架。
- OceanBase 这类工业级数据库常见设计取向:MVCC、日志恢复、LSM/Compaction、复制与主从恢复。
- 公开面经里反复出现的提问方式:不是只问定义,而是顺着一个机制不断追问工程细节与边界条件。
建议按下面顺序复习:
- 先把“索引并发 + 事务隔离 + 日志恢复”串成主线。
- 再补“SSI/OCC、MVCC GC、DDL 与分布式复制”这些更偏设计的问题。
- 最后单独刷“Raft、主从恢复、新节点加入、P99 延迟预算”这类系统题。
结论先看¶
这类面试的核心不是背术语,而是能不能把下面这条链路讲顺:
存储结构 -> 并发控制 -> 可见性 -> 提交/恢复 -> 副本复制 -> 在线变更
如果你回答一个问题时只能停留在“定义”,通常下一问就会卡住;比较稳的答法应该总是包含四层:
- 这个机制解决什么问题。
- 它依赖哪些关键数据结构。
- 它在正常路径怎么工作。
- 它在冲突、崩溃、扩缩容时怎么兜底。
当前仓库内的回看路径¶
| 主题 | 优先回看文件 | 作用 |
|---|---|---|
| bustub 整体框架、MVCC + OCC | docs/blogs/posts/bustub通关指北.md | 把项目实现和面试话术串起来 |
| B+ 树基础 | docs/notes/CMU15445/7-B+Tree.md | 节点结构、分裂合并、查找路径 |
| B+ 树并发 / 蟹行协议 | docs/notes/CMU15445/8-Index Concurrency.md | latch crabbing、乐观改进、叶子扫描并发 |
| 隔离级别 / OCC / 时间戳排序 | docs/notes/CMU15445/15-Timestamp_Ordering_Concurrency.md | OCC 适用场景、验证开销 |
| MVCC / 快照 / GC | docs/notes/CMU15445/16-Mult-Version_Concurrency.md | 多版本可见性、watermark、GC |
| WAL / group commit / checkpoint | docs/notes/CMU15445/17-Database_Logging.md | Redo/WAL 协议、group commit |
| ARIES 恢复 | docs/notes/CMU15445/18-Database_Recovery.md | analysis/redo/undo、checkpoint |
| LSM / Compaction | docs/notes/CMU15445/4-Database_Storage_II.md | leveled / universal(tiered) 对比 |
高频问题总览¶
可以把你列出来的题分成六簇:
- 索引并发:B+ 树蟹行协议、锁和 latch 的区别、为什么从上到下加锁。
- 事务隔离:RC / RR / Serializable、乐观锁和悲观锁、为什么业界更常用悲观事务。
- 串行化实现:SSI 怎么做、前向/后向验证、为什么重点看读写冲突而不是写写冲突、读写集合怎么维护。
- 日志恢复:Redo / Undo / Checkpoint / 原子提交 / group commit / 崩溃恢复。
- 分布式复制:主从下 redo 怎么同步、log truncation、落后节点怎么追、快照怎么补齐。
- 在线变更与共识:create index / add column 怎么做、DDL 与 DML 并发、Raft 提交延迟、follower read、joint consensus。
下面按这个结构展开。
一、索引与并发控制¶
1. B+ 树蟹行协议是什么¶
一句话回答:
蟹行协议(latch crabbing / latch coupling)是 B+ 树并发访问时的一种逐层持锁策略:拿到子节点 latch 后,再根据子节点是否“安全”决定是否释放父节点 latch。
答题骨架:
- 搜索通常拿
R-latch,从根走到叶,拿到子节点读锁后就可以释放父节点。 - 插入 / 删除通常更保守,要判断子节点是否安全。
安全的常见含义:- 插入时子节点未满,不会 split。
- 删除时子节点不至于低于最小占用,不会 merge / redistribution。
- 一旦子节点安全,就可以尽早释放祖先节点,减少高层热点。
- 如果不安全,就需要继续保留祖先节点 latch,保证结构修改期间路径稳定。
为什么重要:
- 根节点和高层内节点竞争最激烈。
- 提前释放父节点能显著提升并发。
- 同时又避免了结构修改期间的悬空指针、路径失效和并发 split/merge 混乱。
常见追问:
lock和latch的区别?lock面向事务语义,保护逻辑数据一致性。latch面向内存临界区,保护页结构不被并发线程破坏。- 为什么释放顺序通常是自顶向下?
- 越高层阻塞越强,优先释放高层 latch 更能减少系统阻塞。
- 更好的优化是什么?
- 15445 里常见“乐观下降到叶子附近,再在必要时回退重试”的 better latching algorithm。
本地回看:
docs/notes/CMU15445/8-Index Concurrency.mddocs/notes/CMU15445/7-B+Tree.md
2. 常见隔离级别怎么讲¶
建议答法:从异常出发,而不是死背名字。
| 隔离级别 | 典型能避免的问题 | 仍可能出现的问题 |
|---|---|---|
| Read Uncommitted | 几乎不保证 | 脏读、不可重复读、幻读 |
| Read Committed | 脏读 | 不可重复读、幻读 |
| Repeatable Read | 脏读、不可重复读 | 取决于实现,很多系统仍可能有幻读或写偏差 |
| Serializable | 目标是等价于某个串行顺序 | 代价最高 |
面试里要补的一句:
真正工程实现里,RR 和 Serializable 不是一个标准答案,而是和实现绑定的。
- MySQL/InnoDB 常通过
MVCC + next-key lock处理很多幻读问题。 - PostgreSQL 的
Serializable不是严格 2PL,而是基于SSI。 - bustub 项目语境里更接近
MVCC + OCC validation去做可串行化。
3. 乐观锁和悲观锁的区别¶
建议从冲突假设、代价位置、失败方式三点回答:
- 悲观锁:
- 假设冲突常见,先加锁再访问。
- 代价前置在执行阶段。
- 等待多,死锁处理重要,但回滚成本通常较低。
- 乐观锁 / OCC:
- 假设冲突少,先执行,提交时验证。
- 代价后置在验证阶段。
- 不容易阻塞,但冲突时可能整事务回滚,长事务代价高。
什么时候悲观事务更好:
- 热点更新多。
- 写事务占比高。
- 事务长、回滚成本高。
- 业务不能接受频繁 abort/retry。
为什么业界悲观事务更多:
- OLTP 热点冲突并不稀少。
- 用户事务常含 RPC / 复杂逻辑,回滚重试代价高。
- 悲观控制延迟更稳定,更容易向业务解释。
- 乐观事务在长事务、高冲突、热点行上 abort 风暴明显。
二、LSM 与 Compaction¶
4. Leveled Compaction 和 Tiered Compaction 的区别¶
先给一句定义:
Leveled:除 L0 外,各层 key range 尽量不重叠,查找更稳,写放大更高。Tiered:同一层允许多个重叠文件,等积累到一定规模再整体合并,写放大更低,读放大更高。
对比主线:
| 维度 | Leveled | Tiered |
|---|---|---|
| 层内重叠 | 基本不重叠 | 允许重叠 |
| 写放大 | 更高 | 更低 |
| 读放大 | 更低 | 更高 |
| 空间放大 | 更低 | 往往更高 |
| 适合场景 | 读多写少、读延迟敏感 | 写多、吞吐优先 |
答题时最好补充:
- RocksDB 里的
Universal Compaction常被看作 tiered 思路。 - 工业系统经常混合策略,不是非黑即白。
- OceanBase 这类系统如果以 LSM 为底座,面试官真正想听的是你能不能把 compaction 选择和“写放大、读放大、空间放大、后台资源竞争”挂钩。
本地回看:
docs/notes/CMU15445/4-Database_Storage_II.mddocs/notes/minilsm/week1-day1.md
三、事务隔离、SSI 与读写集合¶
5. SSI 你怎么实现¶
一句话回答:
SSI(Serializable Snapshot Isolation)是在快照隔离基础上,额外跟踪事务间的读写依赖,发现可能形成危险结构时主动 abort,从而把 SI 提升到 Serializable。
注意:如果面试官前文一直在聊 bustub,这里最好主动澄清一下。bustub 项目更接近
MVCC + OCC validation教学实现,不是 PostgreSQL 那种完整 SSI 工程实现;但二者都在解决“仅有快照可见性还不够,还要补串行化验证”这个问题。
答题骨架:
- 每个事务拿一个快照时间戳
read_ts。 - 读时按 MVCC 规则找
<= read_ts的可见版本。 - 维护读集 / 写集,或更细粒度的读写依赖元数据。
- 提交时检查和并发事务、已提交事务之间是否形成危险依赖边。
- 一旦检测到典型危险结构,回滚其中一个事务。
如果结合 bustub / OCC 语境来答:
- 可以说成
MVCC 负责快照可见性,OCC/SSI 负责提交时串行化验证。 - 面试官更关心你如何维护依赖边,而不是你会不会背 PostgreSQL 术语。
6. 前向验证和后向验证区别¶
前向验证:
- 当前事务提交时,检查自己是否会和未来还活着的事务冲突。
- 关注“我提交后会不会伤到别人”。
后向验证:
- 当前事务提交时,检查在我执行期间已经提交的事务是否和我冲突。
- 关注“别人是否已经破坏了我的快照假设”。
工程里怎么答更稳:
- 后向验证实现更常见,因为提交事务集合是稳定的。
- 前向验证需要看活跃事务的读写状态,代价和同步复杂度更高。
- 实现时常把两者结合成某种窗口化验证,而不是纯理论上的二选一。
7. 为什么 SSI 重点看读写冲突,不是写写冲突¶
关键点:
- 在快照隔离下,写写冲突通常已经在更新或提交阶段被检测了,比如 first-writer-win。
- 真正会漏掉串行化问题的是
rw依赖导致的write skew。 - SSI 的目标不是重复做 WW 检查,而是补上 SI 本来漏掉的危险结构。
一句标准话术:
写写冲突往往已经被底层并发控制或版本检查捕获;SSI 之所以有价值,是因为仅靠快照隔离无法识别跨记录、跨谓词的读写依赖环。
8. 读写集合怎么维护,怎么判断冲突¶
最基础版本:
- 读集:记录读过的 RID、主键、范围谓词。
- 写集:记录插入 / 更新 / 删除过的 RID 或 key range。
- 验证时做交叉检查:
- 我的读集和别人写集是否相交。
- 我的写集和别人读集是否相交。
- 谓词读还要检查 phantom。
更工程化一点的答法:
- 点查可用
RID/key级别集合。 - 范围扫描必须额外记录谓词或索引范围。
- 如果想把验证粒度降到“行级”而不是“事务级”,可以在行或索引项上维护最近读写事务信息。
9. 长事务验证太重,怎么优化¶
你给出的 Tmax 思路是对的,可以直接讲:
- 维护一个可接受验证窗口,例如
[Tmax, GlobalTs]。 - 小于
Tmax的过旧事务不再做全量精细验证,而是让长事务直接 abort 或强制重试。 - 本质上是用吞吐换长事务公平性,避免验证集合无限膨胀。
还能补两个优化:
- 以行 / 索引范围为单位维护冲突元数据,减少无关事务比较。
- 对只读事务使用安全快照或只读优化,尽量不参与完整验证。
10. 如果在行和索引上记录读写事务,这个结构要持久化吗¶
建议答法:
一般不要求把“临时事务依赖状态”完整持久化,因为它属于运行时并发控制元数据,不是最终用户数据。
崩溃恢复后怎么处理,取决于恢复模型:
- 如果没有 commit record,就视为事务未生效,恢复时不用保留它的运行时状态。
- 如果有 redo + commit record,重放已提交事务即可恢复最终可见状态。
- 运行时读写依赖边通常在重启后重建,不需要像数据页一样永久保存。
11. 快照是怎么实现的,可重复读怎么做到的¶
标准主线:
- 每个事务开始时拿到
read_ts或 snapshot。 - 读记录时,从版本链里找到对该快照可见的最新版本。
- 事务期间重复读同一条记录时,只要还是按同一 snapshot 判定,就能实现可重复读。
要主动补一句:
可重复读只解决“同一记录重复读取一致”,不自动等于可串行化;跨行约束和谓词读还可能出现 write skew / phantom,需要 SSI、谓词锁或 next-key lock 进一步兜底。
12. 多版本 GC 怎么实现¶
基础答法:
- 维护 watermark,也就是当前最小活跃读时间戳。
- 版本时间戳早于 watermark 且不再被任何活跃事务引用时,就可以回收。
追问:最老读事务一直不结束怎么办
- GC 会被卡住,版本膨胀。
- 工业系统通常会:
- 给长事务超时。
- 对历史版本设置保留窗口。
- 强制终止过老事务或把它转到特殊慢路径。
四、Redo / Undo / 原子提交 / 恢复¶
13. Redo Log 怎么实现¶
建议答法:
- 每次事务修改数据页前,先生成日志记录。
- 日志里至少包含:事务 ID、页号、LSN、变更内容或逻辑操作。
- 提交时必须先把 commit record 以及之前的相关 WAL 刷盘,再对外宣布成功,这就是 WAL 协议。
关键术语:
pageLSNflushedLSNWAL before datagroup commit
14. Undo Log 怎么实现¶
两种答法都要会:
- 在 ARIES 语境里:
- Undo log / prevLSN 链用来回滚未完成事务。
- abort 时按日志链逆向撤销,并写 CLR。
- 在 bustub 的 MVCC 语境里:
- Undo log 更像版本链的一部分,保存旧值或 delta,供快照读和回滚使用。
- 表堆保存最新值,旧版本挂在 undo/version 链上。
如果面试官问“Redo 和 Undo 分工”
- Redo 保证已提交事务不丢。
- Undo 保证未提交事务不污染最终状态。
15. 事务原子提交怎么实现¶
一句话回答:
原子提交依赖“提交标记”的持久化边界,而不是依赖数据页在提交瞬间全部刷盘。
标准路径:
- 事务执行期间写 WAL。
- 提交时写
COMMIT日志。 - 把该事务对应 WAL 至少刷到持久化介质。
- 刷盘成功后,才把事务状态从
RUNNING改成COMMITTED并返回客户端成功。
为什么这能保证原子性:
- 崩溃恢复时:
- 有 commit record 的事务重做。
- 没有 commit record 的事务撤销或忽略。
16. 怎么保证事务提交过程中的数据对新读者不可见¶
推荐答法:分“物理写入”和“逻辑可见”两层。
- 写线程可以先把新版本写到数据页或私有工作区。
- 但对新读者是否可见,取决于版本元数据 / commit_ts / txn state,而不是是否已经写进页里。
- 只有当事务状态原子切换为
COMMITTED,且版本上的提交时间戳可见后,新事务才会读到它。
换句话说:
“写进内存”不等于“已提交可见”。可见性由事务状态和版本时间戳统一裁决。
17. 事务能并发提交吗,谁来推动提交¶
答案:
- 事务当然可以并发提交,但通常需要一个日志子系统协调刷盘顺序。
- 常见实现是多个事务并行进入提交队列,由 log manager / group commit 线程批量 flush。
- 刷盘完成后,再统一唤醒对应事务返回成功。
面试加分点:
- 并发提交不等于提交顺序任意,仍要保证 LSN 和可见性顺序满足恢复要求。
- 很多系统的吞吐关键点就在 group commit,而不是单事务 fsync。
18. redo log 起点是什么,为什么要 checkpoint¶
标准答案:
- redo 不会从时间 0 开始扫,而是从 checkpoint 相关位置开始。
- checkpoint 记录了恢复所需的最早日志边界,例如脏页表里的最小
recLSN。 - 这样恢复时只重放可能尚未落盘的数据页相关日志,缩短恢复时间。
19. 如果没有事务,批量写多个 KV,怎么保证原子写入¶
面试稳妥答法:
- 可以用
write batch + WAL + 原子安装元数据。 - 典型方案:
- 先把这批 KV 写到 WAL。
- 后台把它们写到 memtable / SSTable。
- 通过一个原子 manifest/version edit 指针切换,使读者只会看到“旧版本”或“新版本”,不会看到半成品。
另外一种答法:
- 用 intent record / batch marker:
- 读者如果看到未完成 batch,就跳过或按旧版本读。
- 只有完成标记出现后,整个 batch 才对外可见。
五、分布式复制、日志截断与新节点恢复¶
20. 一主多从下,redo log 怎么同步¶
按层次答:
- 单机 WAL 解决本地崩溃恢复。
- 分布式下还要解决副本一致性,通常不能只靠“把 redo 文件 rsync 给 follower”。
- 更常见的做法是:
- 主库把逻辑变更或日志条目按顺序复制给从库。
- 从库按相同顺序落盘并应用。
- 提交点由复制协议决定,是本地主库先提交,还是多数副本确认后提交。
如果面试官切到强一致复制:
- 就把它升级为 Raft/Paxos 日志复制来答。
- 事务提交成功的条件变成“日志条目在多数派 durable/accepted”。
21. 不同节点怎么截断 redo log¶
关键点:
- 截断必须晚于“该日志已经被 checkpoint / snapshot 覆盖,且所有需要它恢复的副本都不再依赖它”。
- 主从架构下通常看最慢副本的复制进度。
- 共识系统里通常要结合
applied index、snapshot index、log compaction point判断。
22. 新加入节点怎么同步数据;旧日志被回收了怎么办¶
标准答法:
- 先尝试日志追赶:从某个 log index / LSN 开始补。
- 如果 leader 已经把那段日志 compaction 掉了,新节点就不能只靠增量日志恢复。
- 这时必须走 snapshot / checkpoint / 基线拷贝,再从 snapshot 点之后继续追日志。
一句话总结:
日志只能帮你追“还没被回收的历史”;更老的数据必须靠快照或全量副本同步。
六、DDL、索引构建与 DML 并发¶
23. DDL 怎么实现,以 create index 为例¶
单机常见思路:
- 进入 schema change 状态,注册一条 DDL job。
- 扫描基表,回填已有数据到新索引。
- 回填期间拦截并记录增量 DML。
- 把回填阶段和增量变更 merge。
- 原子切换元数据,让新索引变为可见。
关键不变量:
- 任意时刻,查询要么完全不用该索引,要么看到一个一致的索引。
- 不能让回填窗口内的 DML 漏写到索引。
24. create index 和 DML 并发,怎么保证最终一致¶
单机场景常见答法:
- 双写方案:
- 基表继续写。
- 新索引在回填期间也接收增量写。
- 或者用 change buffer / delta log:
- 先记录索引构建期间发生的增量修改。
- 回填完成后回放增量。
分布式扩展怎么讲:
- DDL 本身必须作为一个全局元数据变更,被所有节点按同一顺序感知。
- 每个节点在收到 DDL 生效前后,必须对本地 DML 打上 schema version。
- 构建索引时按 schema version 划分:
- 某个版本之前的数据做存量回填。
- 之后的数据走增量捕获。
- 最后通过一个全局“切换版本”原子生效。
25. 分布式数据库上 DDL 怎么原子提交,以 add column 为例¶
建议答法:两阶段元数据切换。
- 阶段 1:发布新 schema,但先不让所有读写立即完全切换。
- 阶段 2:等所有节点都确认接收并完成必要准备后,原子推进 schema version。
需要补的工程问题:
- 老 schema 节点如何处理新写入。
- 读请求跨节点时如何协商 schema version。
- 回滚 DDL 时是回滚元数据,还是需要补数据清理。
七、Raft 与系统设计追问¶
26. 三节点系统多数派提交,单机 P99 要做到多少才有希望整体 P99 < 1ms¶
这题通常不是要你算精确闭式解,而是要你意识到:
- 三节点提交至少包含:
- leader 本地处理与落盘
- 两条网络路径中的较慢一条
- follower 落盘 / ack
- 分布式 P99 不会优于其关键路径上各子项的 P99。
- 如果整体目标是
< 1ms,那单机本地 WAL flush、网络 RTT、follower 落盘都必须远低于 1ms,通常要把预算拆得非常细,单机侧往往只能占几百微秒量级。
面试里别给死数,先讲预算拆分。
27. follower 可以提供读吗¶
答案:可以,但要分一致性等级。
- 弱一致读:直接读 follower,本质上允许陈旧。
- 线性一致读:必须额外证明 follower 没落后到违反 lease / commit index 约束。
Raft 语境下常见做法:
- leader read
- read index
- lease read
28. 新增节点为什么要 joint consensus¶
一句话:
为了避免配置变更过程中出现“旧多数派”和“新多数派”不相交,导致系统在过渡态出现脑裂或双重提交。
答题骨架:
- 配置从
C_old到C_new不能一步切。 - 先进入联合配置
C_old,new。 - 该阶段需要同时满足联合配置下的提交规则。
- 再切到
C_new。
八、可以直接背的高频短答¶
29. 基于 B+ 树的数据库存储引擎怎么工作¶
可以按这个顺序说:
- 表和索引以页为单位组织,B+ 树内节点做导航,叶子节点存 key -> RID 或行。
- Buffer Pool 缓存热点页,磁盘层负责页读写。
- 并发访问时用 latch 保护页结构,用 lock/MVCC 保护事务语义。
- 更新会写 WAL;若支持 MVCC,还会维护版本链或 undo。
- 崩溃后通过 redo/undo 或 ARIES 恢复。
30. 什么情况下悲观事务比乐观事务好¶
- 热点写多。
- 冲突率高。
- 长事务多。
- 回滚代价高。
- 业务延迟抖动比平均吞吐更重要。
31. 为什么业界悲观事务更多¶
- 更符合真实 OLTP 冲突分布。
- 对业务更稳定。
- 对长事务更友好。
- 不容易出现大量 retry 风暴。
九、补充专题:15445 / MySQL / RocksDB¶
32. CMU-15445 有做实际的数据存储吗¶
可以答“有,但它是教学型实现,不是工业级完整产品”。
- bustub 里有真正的页式存储、slotted page、buffer pool、disk manager、B+ 树索引。
- 表数据和索引数据都不是“纯内存玩具结构”,而是按页组织并能落到磁盘文件。
- 但它的目标是教学,不会把工业系统里的恢复、后台线程、在线 DDL、复制、复杂优化器全做全。
面试更稳的表述:
15445/bustub 已经覆盖了一个数据库内核最关键的存储闭环:page -> buffer pool -> table heap / index -> transaction visibility,只是很多地方做了教学性简化。
33. CMU-15445 的并发控制怎么实现¶
先分层回答:
- 索引页结构并发:靠
latch,比如 B+ 树的 latch crabbing。 - 事务隔离与可见性:靠
MVCC。 - 串行化验证:在 bustub 项目语境里更接近
MVCC + OCC validation。
可以直接背的版本:
- 读事务拿快照时间戳
read_ts。 - 写事务先改最新版本,同时生成 undo/version 信息。
- 读时按版本链找对当前快照可见的版本。
- 提交时分配
commit_ts,并做 OCC 冲突检查。 - 冲突则 abort,不冲突则提交并让新版本对后续事务可见。
34. 版本链的存储是怎么做的¶
bustub 语境下的标准答法:
- 表堆里始终存“当前最新 tuple”。
- 旧版本不直接堆在表页里,而是以
UndoLog的形式挂成版本链。 TxnManager维护某条记录最新 undo/version 链接的入口。- 每个事务自己的工作区里保存它生成的 undo logs。
- 链上的节点通常记录旧值或 delta,以及指向上一个版本的链接。
一句话总结:
base tuple 在表里,历史版本在 undo 链里,入口指针在事务管理器维护的版本元数据里。
35. 版本链是持久化的数据吗,还是只在内存里¶
如果面试官问的是 bustub 项目:
- 更准确的说法是:它主要是教学用的运行时 MVCC 结构,不是一个完整工业级持久化版本存储系统。
- 表页数据会落盘,但版本链元数据和事务运行时状态主要在内存管理结构里维护。
- 所以它不能直接等价类比到 InnoDB 那种“完整 crash recovery 后仍能严密恢复事务状态”的工程系统。
如果换成工业数据库:
- 历史版本有的体现在 undo segment / rollback segment 中。
- 有的通过 WAL + page image + 版本元数据重建。
- 是否“原样持久化整条版本链”取决于具体实现,不是所有系统都一样。
36. 拿到 tuple 后怎么找到版本链¶
标准路径:
- 先拿到 tuple 的
RID,也就是(page_id, slot)。 - 用
page_id/slot到事务管理器维护的版本信息里查入口。 - 入口通常是一个
UndoLink,包含类似(txn_id, log_index)的定位信息。 - 再跳到对应事务持有的 undo log 数组或链表里,沿着
prev_version一路往前找。 - 找到第一个对当前
read_ts可见的版本后返回。
一句标准话术:
tuple 自身只负责给你当前版本和 RID;真正的历史链入口通常要靠 RID 去版本目录里查,再顺着 undo link 追。
37. MySQL 索引实现原理¶
面试里如果只说 “MySQL”,默认最好主动切到
InnoDB,因为事务、MVCC、聚簇索引这些都属于 InnoDB 引擎层。
核心点:
- InnoDB 的索引底层是
B+ 树。 - 主键索引是
聚簇索引,叶子页直接存整行记录。 - 二级索引叶子页存的是
二级键 + 主键值,不是整行地址。 - 走二级索引查整行时,通常要再根据主键回到聚簇索引,这就是回表。
- 如果查询列都在二级索引里,就能覆盖索引,避免回表。
为什么这样设计:
- 聚簇索引让主键范围扫描局部性更好。
- 二级索引带主键而不是物理地址,避免行移动后大量失效。
38. 什么是 B+ 树¶
一句话:
B+ 树是一种多路平衡搜索树,内节点只存导航键,所有真实数据都在叶子节点,叶子节点之间通常还按链表连接,适合磁盘/页式存储。
为什么数据库爱用它:
- 树高低,磁盘 IO 少。
- 范围扫描友好。
- 单页可放很多 key,扇出高。
- 比普通二叉搜索树更稳定,也比哈希更适合范围查询。
39. MySQL 事务怎么实现¶
按模块回答最稳:
- 原子性:
undo log - 持久性:
redo log - 隔离性:
MVCC + lock manager - 一致性:靠约束、应用逻辑和前面几项共同保证
如果再展开一步:
- 更新前先写 undo,用于回滚和快照读。
- 更新时写 redo,保证崩溃后已提交事务可恢复。
- 普通一致性读走 MVCC。
- 当前读、更新、删除走锁。
- 提交时通过 redo 刷盘保证 durability。
40. MySQL 的 MVCC 是什么¶
一句话:
InnoDB 的 MVCC 是“数据行隐藏字段 + undo log 版本链 + Read View”共同实现的多版本并发控制。
标准答法:
- 每行记录有隐藏版本信息,例如创建事务 ID、回滚指针。
- 更新时不会把旧版本直接抹掉,而是把旧值放进 undo log,形成历史版本链。
- 一致性读根据 Read View 判断当前版本是否可见。
- 如果不可见,就顺着回滚指针去 undo 链里找更老版本。
41. MySQL 的强项是什么,它怎么做到的¶
推荐答法:
MySQL 的强项是成熟稳定的 OLTP 能力,而不是单点极致理论性能。
它能做好的原因主要有:
- InnoDB 的
B+ 树 + Buffer Pool带来成熟稳定的页式存储访问路径。 MVCC + 锁在读多写多混合场景下比较均衡。Redo Log + Group Commit + Doublewrite让崩溃恢复和持久化路径比较可靠。- 复制、备份、生态工具、可观测性和运维经验很成熟。
42. MySQL 容灾恢复怎么做¶
分两层回答:
- 单机崩溃恢复:
- 重启后根据 redo log 重做已提交但未落盘的数据页修改。
- 根据 undo / 事务状态处理未完成事务。
- 集群级容灾:
- 用 binlog 复制做主从。
- 可配半同步、MGR / Group Replication、备份加 binlog 回放做时间点恢复。
一句话:
本地 crash recovery 靠 redo/undo,实例级高可用靠复制,误操作恢复靠备份 + binlog。
43. MySQL 写磁盘为什么性能还能很高¶
关键在于它把“随机数据页写”转成了“顺序日志写优先”。
- 提交关键路径先写 redo log,redo 是追加写,顺序 IO 友好。
- 真正的数据页刷新可以延后,由后台线程批量刷脏页。
- 多个事务可以
group commit,共享一次 fsync。 - 二级索引写还有 change buffer 这类优化可减少随机 IO。
- Buffer Pool 吸收了大量热点读写,不是每次更新都直写数据页。
一句加分话术:
高性能不是因为“它把所有数据都写得很快”,而是因为它尽量把提交路径压缩成顺序 WAL,并把昂贵的数据页落盘异步化、批量化。
44. MySQL 事务与可重复读怎么实现¶
可以直接背:
- 可重复读下,事务第一次一致性读会生成一个
Read View。 - 后续一致性读都复用这同一个 Read View。
- 所以同一事务内多次普通
SELECT,看到的是同一快照。 - 如果是当前读,例如
SELECT ... FOR UPDATE、UPDATE、DELETE,则读的是最新版本并加锁。 - 为了防很多幻读问题,InnoDB 还会配合
next-key lock。
45. MVCC 在 RR 和 RC 下有什么区别¶
最核心的区别就是 Read View 的生命周期。
RC:- 每条语句生成新的 Read View。
- 同一事务里前后两次普通查询,可能看到后来提交的新版本。
RR:- 一般是事务级 Read View。
- 同一事务里的普通一致性读看到同一快照。
所以结果上的差异:
- RC 更容易看到“别人刚提交的新数据”。
- RR 更强调事务内读一致性。
- 但 RR 也不自动等于完全 Serializable,仍要看锁和实现细节。
46. RocksDB 底层文件的存储格式¶
先从目录级别答:
WAL:写前日志。SST/SSTable:真正存数据的不可变有序文件。MANIFEST:记录版本元数据和文件集合变化。CURRENT:指向当前使用的 MANIFEST。- 还有
OPTIONS、LOCK、LOG等辅助文件。
再从 SST 文件内部答:
- Data Block:真正的 key-value 数据块。
- Index Block:帮助定位数据块。
- Filter Block:通常是布隆过滤器。
- Metaindex / Properties:元数据。
- Footer:文件尾部索引入口。
一句话:
RocksDB 不是“一个大文件”,而是 WAL + 多个 immutable SST + 版本元数据 共同组成的 LSM 存储集合。
47. RocksDB 优劣势¶
优势:
- 写吞吐高,特别适合高写入 KV 场景。
- WAL + MemTable + SST 的路径天然适合顺序写。
- 压缩、布隆过滤器、compaction 策略、column family 等可调性很强。
- 嵌入式使用方便,很多分布式系统直接拿它做本地状态机存储。
劣势:
- compaction 带来的写放大、读放大、空间放大很难完全避免。
- 参数很多,调优成本高。
- 后台 compaction 资源竞争重时,容易出现写 stall、尾延迟抖动。
- 复杂事务、多表关系、二级索引能力不如成熟关系型引擎自然。
一句总结:
RocksDB 很强,但强在“高性能嵌入式 LSM KV 引擎”;如果需求是完整关系型事务数据库能力,就通常还得在它上面再搭一层数据库内核。
十、最后怎么复习最有效¶
建议你按三轮来刷:
- 第一轮只刷主线:
B+ 树并发 -> 隔离级别 -> MVCC/OCC/SSI -> WAL/ARIES - 第二轮专门刷追问:
读写集合怎么维护 -> 快照怎么做 -> watermark/GC -> group commit -> checkpoint - 第三轮刷系统题:
主从复制 -> 新节点恢复 -> DDL 并发 -> Raft read / membership change -> P99 预算
如果时间有限,优先级最高的是下面 10 题:
- B+ 树蟹行协议。
- RC / RR / Serializable 区别,以及 RR 为什么不一定等于 Serializable。
- 乐观锁和悲观锁,为什么工业界更常用悲观事务。
- MVCC 快照怎么做,可重复读怎么保证。
- SSI 为什么关注读写冲突。
- 读写集合怎么维护,长事务怎么优化验证。
- Redo / Undo / WAL / checkpoint / ARIES 主线。
- 原子提交为什么靠 commit record,而不是靠数据页同时刷盘。
- 新节点加入时为什么需要 snapshot,日志回收后为什么不能只靠 redo。
- create index 和 DML 并发时怎么保证索引不丢数据。
十一、公开资料¶
下面这些外部资料适合和仓库内笔记交叉着看:
- CMU 15-445 课程主页:https://15445.courses.cs.cmu.edu/
- PostgreSQL Serializable / SSI 文档:https://www.postgresql.org/docs/current/transaction-iso.html
- PostgreSQL SSI 论文:https://dl.acm.org/doi/10.1145/1620585.1620587
- RocksDB Compaction 文档:https://github.com/facebook/rocksdb/wiki/Compaction
- OceanBase 文档中心:https://www.oceanbase.com/docs
- OceanBase 存储引擎与事务博客:https://en.oceanbase.com/blog/2597046272、https://en.oceanbase.com/blog/2615446272
- Raft 扩展论文与成员变更:https://raft.github.io/
- OceanBase 公开面经话题页(用于看提问风格,不作为技术正确性的唯一来源):https://www.nowcoder.com/creation/subject/d7a72080186a4c3cb7e6b445602b33e7