八股文解析
MySQL MVCC 是怎么实现的?Read View 如何判断可见性?
一句话结论
MVCC 通过隐藏列(DB_TRX_ID/DB_ROLL_PTR)+ undo log 版本链 + Read View 一致性视图,实现读写不互斥的快照读。
面试标准答法
一、MVCC 要解决什么问题
MVCC(Multi-Version Concurrency Control,多版本并发控制)核心目标是:读不阻塞写,写不阻塞读。在 InnoDB 中,普通 SELECT 是快照读(Snapshot Read),不加锁;UPDATE/DELETE/INSERT 是当前读(Current Read),加锁。MVCC 让快照读通过版本链 + Read View 判断可见性,完全避开锁竞争。
二、三个核心机制
1. 隐藏列(Hidden Columns)
InnoDB 每行记录有三个隐藏字段(非用户定义):
| 隐藏列 | 作用 |
|---|---|
| DB_TRX_ID(6字节) | 最近一次修改(插入/更新/删除)该行的事务 ID |
| DB_ROLL_PTR(7字节) | 回滚指针,指向 undo log 中该行的前一版本 |
| DB_ROW_ID(6字节,可选) | 无主键时自动生成的行 ID,有主键则不占 |
关键点:DELETE 不是物理删除,而是标记删除——在 DB_TRX_ID 记录删除事务 ID,并生成一个版本到 undo log。
2. undo log 版本链
每次 UPDATE/DELETE 都会将旧版本数据写入 undo log,DB_ROLL_PTR 串起所有历史版本,形成单向链表:
[最新版本] DB_TRX_ID=T3, DB_ROLL_PTR → [版本2] DB_TRX_ID=T2 → [版本1] DB_TRX_ID=T1 → NULL注意:undo log 分两种——insert undo log(插入时生成,事务提交后可直接清理)和 update undo log(更新/删除时生成,需 MVCC 使用,不能立即清理)。
3. Read View(一致性视图)
Read View 是事务执行快照读时生成的活跃事务快照,包含四个关键字段:
| 字段 | 含义 |
|---|---|
| m_low_limit_id | 当前已分配的最大事务 ID + 1(即下一个事务 ID) |
| m_up_limit_id | 活跃事务列表中最小的事务 ID |
| m_ids | 生成 Read View 时所有活跃事务 ID 列表 |
| m_creator_trx_id | 创建该 Read View 的事务自身 ID |
三、可见性判断规则(核心中的核心)
遍历版本链,对每个版本执行以下判断:
if (DB_TRX_ID == m_creator_trx_id)
→ 自己修改的,可见
else if (DB_TRX_ID < m_up_limit_id)
→ 事务已提交,可见
else if (DB_TRX_ID >= m_low_limit_id)
→ 在 Read View 生成之后才开始的事务,不可见
else if (DB_TRX_ID ∈ m_ids)
→ 事务未提交,不可见
else
→ 事务已提交(在 m_up_limit_id 和 m_low_limit_id 之间但不在活跃列表),可见简化记忆:比自己早的且已提交的可见,比自己晚的或未提交的不可见,自己改的永远可见。
四、RC 与 RR 的差异
| 隔离级别 | Read View 生成时机 | 效果 |
|---|---|---|
| READ COMMITTED(RC) | 每次快照读都生成新 Read View | 能看到其他事务已提交的最新数据 |
| REPEATABLE READ(RR) | 事务第一次快照读时生成,后续复用 | 整个事务看到同一份快照,解决不可重复读 |
这就是为什么 RR 能避免不可重复读,而 RC 不能——本质是 Read View 的复用策略不同。
对比表格:MVCC vs 锁机制
| 维度 | MVCC 快照读 | 悲观锁(SELECT FOR UPDATE) |
|---|---|---|
| 并发度 | 读写完全并行,无阻塞 | 读读并行,读写/写写互斥 |
| 实现成本 | 依赖 undo log,存储开销大 | 依赖锁结构,内存开销小 |
| 一致性 | 快照一致性,非实时 | 实时一致性(当前读) |
| 适用场景 | 读多写少,报表查询 | 资金操作、库存扣减等强一致场景 |
| 死锁风险 | 无 | 有(需死锁检测) |
| 隔离级别 | RC/RR 均支持 | 所有级别 |
常见追问
| 追问 | 回答要点 |
|---|---|
| RR 下为什么没有幻读? | RR 通过 MVCC 解决快照读的幻读;但当前读(SELECT FOR UPDATE)仍需 Gap Lock + Next-Key Lock 解决。MVCC 和 Next-Key Lock 是两套机制,配合使用。 |
| undo log 什么时候清理? | 由 purge 线程异步清理。判断条件:版本链上所有版本都不再被任何活跃 Read View 引用(即所有版本对当前活跃事务都不可见)。长事务会导致 undo log 膨胀。 |
| Read View 生成时活跃事务列表怎么维护? | InnoDB 维护全局活跃事务链表(trx_sys->rw_trx_list),Read View 生成时拷贝该链表快照。注意:只包含读写事务,只读事务不参与。 |
| 为什么 RC 下每次读都要新生成 Read View? | 因为 RC 的语义是"读已提交的最新数据",如果复用旧 Read View,就读不到其他事务后续提交的数据,不符合 RC 定义。这是 SQL 标准的要求。 |
面试回答模板(30 秒版)
延伸准备
- undo log 物理结构:深入理解 undo log 的段(segment)、页(page)组织方式,以及 insert undo 和 update undo 在回收策略上的差异,能体现底层功底。
- 一致性读 vs 锁定读的源码路径:
row_search_mvcc()函数的执行流程,以及lock_clust_rec_read_check_and_lock()如何与 MVCC 配合,能区分"资深"和"中级"。
- MVCC 与索引的关系:二级索引(Secondary Index)没有 DB_TRX_ID,InnoDB 如何通过回表 + 主键版本链判断二级索引记录的可见性?这涉及
row_sel_build_prev_row()的细节逻辑。
- 分布式事务下的 MVCC 局限:XA 事务中 MVCC 如何与全局事务 ID 交互,以及为什么分布式场景下通常需要额外的一致性方案。
想系统备战大厂大模型/Agent 开发?NiceOffer 提供 SDE+LLM 双轨 1v1 陪跑,合同保底 40w 年薪,文末扫码咨询。