AI 模拟面试实战:MySQL MVCC(多版本并发控制)底层源码级实现:UndoLog 链条与 ReadView 隔离规则
在 MySQL 数据库与高并发存储底层原理面试中,MVCC(Multi-Version Concurrency Control,多版本并发控制)是技术面试官用来考察候选人对“事务隔离级别实现机理与无锁快照读”理解深度的绝对核心必考点。
很多同学在面试中能背诵出:
- “MVCC 用于实现读已提交(Read Committed,RC)和可重复读(Repeatable Read,RR)隔离级别”;
- “依靠隐藏字段、UndoLog 版本链和 ReadView 快照视图实现”。
但当大厂面试官在白板上给出具体的并发事务执行时序,并追问底层源码细节:
“每行记录的 3 个隐藏字段(
DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID)到底占用多少字节?ReadView内部维护的 4 个核心属性(m_ids、min_trx_id、max_trx_id、creator_trx_id)是如何通过 4 条严格的可见性比对算法,判定 UndoLog 链条上的某个历史版本对当前事务是可见还是不可见的?在 RC 隔离级别与 RR 隔离级别下,ReadView的生成时机到底有什么本质区别?为什么 MySQL RR 级别能基本解决幻读(Phantom Read)但依然存在‘幻读边缘 Case’?”
很多背八股文的同学就会在四步判定逻辑与版本链回溯细节上当场卡壳。
今天我们通过 AI 模拟面试官的深度推演视角,把 MySQL InnoDB 引擎中 MVCC 的底层执行时序与 ReadView 判定算法彻底讲透。
核心考点一:InnoDB 聚簇索引的 3 个物理隐藏字段
在 InnoDB 的 B+ 树叶子节点中,每一条聚簇索引记录除了用户定义的业务列外,系统都会物理追加 3 个系统级隐藏字段:
graph LR subgraph 物理聚簇索引行记录 (Clustered Index Record) Col1[业务字段: id] Col2[业务字段: name] Col3[业务字段: age] H1[DB_TRX_ID: 6 字节<br>记录最后一次插入/修改该记录的全局事务 ID] H2[DB_ROLL_PTR: 7 字节<br>回滚指针: 指向 undo log 中上一版本的物理地址] H3[DB_ROW_ID: 6 字节<br>隐藏自增行 ID (若表无主键与唯一索引时自动生成)] end核心考点二:UndoLog 版本链(Undo Log Chain)的物理编织
当一条记录被不同事务并发修改时,InnoDB 不会直接覆盖旧数据,而是将修改前的旧镜像写入 Undo 页中,并通过DB_ROLL_PTR指针串联成一条单向链表(从最新版本指向最老版本):
graph TD Latest[最新物理行记录 (当前数据页中): name='张三丰', DB_TRX_ID=300] -->|DB_ROLL_PTR| Undo1[UndoLog 历史版本 1: name='张三', DB_TRX_ID=200] Undo1 -->|DB_ROLL_PTR| Undo2[UndoLog 历史版本 2: name='张小三', DB_TRX_ID=100] Undo2 -->|DB_ROLL_PTR| NullNode[NULL (最初插入时的版本)]核心考点三:ReadView 核心数据结构与四步可见性判定算法
ReadView是事务在执行快照读(SELECT)时,由 InnoDB 内存引擎动态创建的快照读一致性视图(Snapshot Read View)。
ReadView 的 4 个核心属性:
m_ids:在生成 ReadView 的那一瞬间,系统中所有活跃且未提交的事务 ID 列表(Active Transaction IDs);min_trx_id:m_ids列表中的最小值(当前系统活跃事务中最早开启的那个事务 ID);max_trx_id:在生成 ReadView 时,系统应该分配给下一个新事务的事务 ID(即当前已分配最大事务 ID + 1);creator_trx_id:创建当前 ReadView 的事务自身的 ID。
终极四步可见性比对算法(Visibility Comparison Algorithm)
当一个事务尝试读取某一行数据时,它顺着 UndoLog 版本链,从最新版本开始,逐一提取该版本的trx_id = DB_TRX_ID,并带入以下 4 步规则进行严格判定:
graph TD Start[提取版本记录的 trx_id] --> Step1{1. trx_id == creator_trx_id ?} Step1 -->|是| Visible1[可见! (是自己修改的数据, 必须能看到)] Step1 -->|否| Step2{2. trx_id < min_trx_id ?} Step2 -->|是| Visible2[可见! (在生成快照前, 该事务早已提交完毕!)] Step2 -->|否| Step3{3. trx_id >= max_trx_id ?} Step3 -->|是| InVisible1[不可见! (该事务在快照生成之后才开启, 属未来事务!)] Step3 -->|否| Step4{4. trx_id 是否在活跃列表 m_ids 中?} Step4 -->|在 m_ids 中| InVisible2[不可见! (在生成快照时, 该事务仍在运行且未提交!)] Step4 -->|不在 m_ids 中| Visible3[可见! (说明该事务在快照前已经成功 Commit!)] InVisible1 & InVisible2 --> NextUndo[顺着 DB_ROLL_PTR 查找下一个更老的 Undo 版本, 重新判定!]核心考点四:RC 与 RR 隔离级别的本质区别(ReadView 生成时机)
这是区分 RC 与 RR 隔离级别在底层实现上的唯一决定性分水岭:
| 隔离级别 | ReadView的生成时机与生命周期 | 是否存在不可重复读? | 核心行为特征 |
|---|---|---|---|
| 读已提交(Read Committed,RC) | 在事务内的【每一次SELECT查询时】,都会重新生成一个全新的ReadView! | 存在(两次查询之间其他事务提交了数据,第二次查询生成了新 ReadView 从而读到了新数据) | 每次读都能看到最新的已提交快照 |
| 可重复读(Repeatable Read,RR) | 仅在事务执行【第一次SELECT快照读时】生成全局唯一的ReadView,并在整个事务执行期间一直复用该视图,永不更新! | 彻底消除(后续所有查询均基于最初的快照判定,其他事务无论怎么提交都不可见) | 保证整个事务期间读到的一致性快照完全相同 |
核心考点五:MySQL RR 级别下是否 100% 解决了幻读?(经典边缘 Case)
在标准 SQL 定义中,RR 级别无法解决幻读。
MySQL InnoDB 通过MVCC(快照读通过 ReadView 避免幻读)+ Next-Key Lock(当前读通过行锁 + 间隙锁 Gap Lock 避免幻读),在 99% 的场景下消除了幻读。
但在极端边缘 Case 下,幻读依然会发生(当前读穿透快照读):
- 事务 A 开启,执行
SELECT * FROM t_user WHERE id = 10(查无此人,生成 ReadView); - 事务 B 插入了一条
id = 10, name = '李四'并成功 Commit; - 事务 A 执行了一条当前读的更新语句:
UPDATE t_user SET age = 20 WHERE id = 10;- 根据当前读规则,事务 A 成功修改了事务 B 刚插入的这条数据,并将该记录的
DB_TRX_ID变为了事务 A 自身的 ID!
- 根据当前读规则,事务 A 成功修改了事务 B 刚插入的这条数据,并将该记录的
- 事务 A 再次执行
SELECT * FROM t_user WHERE id = 10:- 判定算法规则一命中:
trx_id == creator_trx_id(是自己刚才更新的数据!); - 事务 A 突然读出了这条原本不存在的
id = 10的记录!发生了经典的“幻读穿透”!
- 判定算法规则一命中:
模拟面试复盘
回答 MySQL MVCC,牢记四大段落:
- 三大隐藏字段:
DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID; - UndoLog 版本链:单向指针历史链表;
- ReadView 四大属性与四步可见性判定:
min_trx_id、max_trx_id、m_ids、creator_trx_id; - RC vs RR 本质:每次 SELECT 生成新 ReadView vs 首次 SELECT 复用唯一 ReadView。
源码级逻辑闭环、因果严密,尽显资深数据库专家水准。