InnoDB MVCC 机制详解
一、MVCC 是什么
MVCC(Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 为实现高并发读写而采用的核心机制。它的基本思想是:
写操作不覆盖旧数据,而是生成新版本;读操作依据自己的"快照"规则,选择对自身可见的版本。
在 InnoDB 中,MVCC 主要服务于 READ COMMITTED(RC)和 REPEATABLE READ(RR)两个隔离级别下的普通 SELECT 查询(即"快照读"),让读操作在不加锁的情况下,也能读到一致性视图,从而实现:
- 读不阻塞写
- 写不阻塞读
二、MVCC 解决什么问题
2.1 没有 MVCC 时的困境
传统并发控制依赖加锁:读加共享锁、写加排他锁,读写互相阻塞。在读多写少的 OLTP 场景中,大量读请求会被写操作阻塞,吞吐量受限。
2.2 MVCC 的解法
写操作创建新版本而非原地覆盖,读操作读取事务开始时刻的一致性快照。读写作用于不同版本,自然不再互相等待。
2.3 MVCC 的能力边界
MVCC 解决的是读写冲突 ,以下问题它不解决:
| 问题 | 说明 |
|---|---|
| 写写冲突 | 两个事务同时改同一行最新版本,仍需行锁串行化 |
| 当前读的幻读 | SELECT ... FOR UPDATE、UPDATE、DELETE 等当前读需配合 Next-Key Lock |
| 写偏斜(Write Skew) | 快照隔离 ≠ 可串行化,MVCC 本身不检测写偏斜 |
| 长事务负担 | 长事务的旧 Read View 会"钉住"旧版本,导致 Undo Log 无法清理 |
三、核心组件
InnoDB 的 MVCC 由三个组件协同实现:隐藏列 、Undo Log 版本链 、Read View。
3.1 隐藏列
InnoDB 在每行记录上默默维护着与 MVCC 直接相关的隐藏字段:
| 隐藏列 | 大小 | 作用 |
|---|---|---|
DB_TRX_ID |
6 字节 | 记录最后一次插入或修改该行数据的事务 ID |
DB_ROLL_PTR |
7 字节 | 回滚指针,指向 Undo Log 中该行上一个版本的位置 |
DB_ROW_ID |
6 字节 | 行 ID(仅在表无主键且无唯一索引时生成,与 MVCC 无关) |
注意 :
DELETE在 InnoDB 中不是物理删除,而是通过标记位将行"标记为已删除",真正的清除由后台 Purge 线程完成。
3.2 DB_ROLL_PTR 的存储结构
DB_ROLL_PTR 是一个 7 字节(56 位)的编码地址,内部包含定位一条 Undo 记录所需的全部坐标:
| 位段 | 长度 | 含义 |
|---|---|---|
| 低位偏移量(Offset) | 16 位 | Undo 记录在所在 Undo 页面内的偏移位置 |
| 页号(Page Number) | 32 位 | Undo 记录所在的 Undo 页面编号 |
| 回滚段 ID(Rollback Segment ID) | 7 位 | Undo 记录所属的回滚段 |
| 最高位标志(Insert Flag) | 1 位 | 标识 Undo 记录类型是 INSERT 还是 UPDATE |
3.3 Undo Log 与版本链
写入时机
Undo Log 的数据不是事务提交时才写入的 ,而是在执行 INSERT、UPDATE、DELETE 的过程中实时写入的。具体流程:
- 先将"如何撤销这次修改"所需的信息写入 Undo Log;
- 再修改当前行的用户字段为新值;
- 更新当前行的
DB_TRX_ID为当前事务 ID; - 让
DB_ROLL_PTR指向刚写入的 Undo 记录。
两类 Undo 的清理差异
| Undo 类型 | 用途 | 清理时机 |
|---|---|---|
INSERT Undo |
仅事务回滚时需要 | 事务提交后即可丢弃(不需要为其他事务提供历史版本) |
UPDATE/DELETE Undo |
回滚 + MVCC 一致性读 | 只要还有活跃事务的 Read View 可能需要它,就不能被清理;确认无快照需要后由 Purge 线程删除 |
版本链的形成
一行数据被多个事务连续修改时,每次修改都在 Undo Log 中留下一条记录,DB_ROLL_PTR 像链条一样把各历史版本串联起来:
当前行(最新版本)
│ DB_TRX_ID = 200(未提交)
│ DB_ROLL_PTR ──┐
▼ │
Undo 记录 v2 │
DB_TRX_ID = 100 │
DB_ROLL_PTR ──┐ │
▼ │ │
Undo 记录 v1 │ │
DB_TRX_ID = 80│ │
DB_ROLL_PTR = NULL
链头是当前最新数据,链尾是最早的旧数据。版本链上每个节点都同时带有 DB_TRX_ID(裁判)和 DB_ROLL_PTR(向导),各自在不同环节发挥作用。
3.4 Read View
版本链提供了"有哪些版本",Read View 则决定"当前事务能看见哪个版本"。它是事务进行快照读时生成的一个可见性规则集,包含四个核心字段:
| 字段 | 含义 |
|---|---|
m_ids |
生成 Read View 时,所有活跃(已启动未提交)读写事务的 ID 集合 |
min_trx_id |
m_ids 中的最小事务 ID |
max_trx_id |
系统下一个要分配的事务 ID(高水位线) |
creator_trx_id |
创建该 Read View 的事务 ID |
四、可见性判断规则
有了 Read View,InnoDB 在读取一行数据时,从版本链的最新记录开始 ,用该记录的 DB_TRX_ID 与 Read View 的规则逐一比对,寻找第一个可见的版本:
| 序号 | 条件 | 结论 |
|---|---|---|
| 1 | DB_TRX_ID == creator_trx_id |
是当前事务自己的修改,可见 |
| 2 | DB_TRX_ID < min_trx_id |
修改该版本的事务在 Read View 生成前已提交,可见 |
| 3 | DB_TRX_ID >= max_trx_id |
修改该版本的事务在 Read View 生成后才启动,不可见 |
| 4 | DB_TRX_ID 在 m_ids 中 |
修改该版本的事务在 Read View 生成时仍活跃(未提交),不可见 |
| 5 | DB_TRX_ID 不在 m_ids 中,且介于 min_trx_id 和 max_trx_id 之间 |
该事务在 Read View 生成前已提交,可见 |
判断流程:
- 定位当前行最新版本,读取其
DB_TRX_ID; - 与 Read View 比对:可见则直接返回当前行值,不碰 Undo Log;
- 不可见则顺着
DB_ROLL_PTR回溯到上一个 Undo 版本 ,读该版本的DB_TRX_ID继续比对; - 重复直到找到可见版本或版本链结束。
关键理解 :所谓"读当前行还是读 Undo",不是二选一的选择,而是版本链遍历的结果。最新版本可见就读当前行,不可见就回溯 Undo 直到找到可见版本。
五、RC 与 RR 的实现差异
MVCC 在 RC 和 RR 下的可见性判断算法完全相同 ,唯一的区别在于 Read View 的生成时机:
| 隔离级别 | Read View 生成时机 | 效果 |
|---|---|---|
| READ COMMITTED (RC) | 每次 执行 SELECT 时都生成一个新的 Read View |
能读到其他事务在本次 SELECT 前已提交的最新数据,因此会出现"不可重复读" |
| REPEATABLE READ (RR) | 第一次 执行 SELECT 时生成,之后整个事务复用这个 Read View |
整个事务期间看到同一个快照,即使其他事务提交也看不到,保证"可重复读" |
RR 级别下 MVCC 解决的是快照读 的幻读问题;对于当前读 (
SELECT ... FOR UPDATE、UPDATE、DELETE),InnoDB 还需配合 Next-Key Lock 来处理。
六、一次查询的完整过程(示例)
假设行 R 初始值为 v1,事务 100 将其改为 v2 并提交,事务 200 又将其改为 v3 但未提交。
当前行状态 :用户字段 = v3,DB_TRX_ID = 200,DB_ROLL_PTR → Undo 记录(含 v2,DB_TRX_ID = 100,再指向含 v1 的 Undo)。
场景 A:事务 300 查询(Read View 中 200 活跃、100 已提交)
- 当前行
DB_TRX_ID = 200→ 在活跃列表m_ids中 → 不可见; - 顺
DB_ROLL_PTR回溯到v2版本,DB_TRX_ID = 100→ 已提交 → 可见; - 返回
v2。
场景 B:事务 400 查询(在事务 200 提交后生成 Read View)
- 当前行
DB_TRX_ID = 200→ 已提交 → 可见; - 直接返回
v3,不回溯 Undo Log。
七、辅助机制与注意事项
7.1 Purge 机制
DELETE 或 UPDATE 产生的旧版本不会立即消失。后台 Purge 线程 会在确认没有任何活跃事务的 Read View 还需要这些旧版本之后,才物理清理它们。
- 如果存在长事务 ,其旧 Read View 会"钉住"旧版本,导致 Undo Log 无法清理,引发 History list 膨胀和磁盘空间增长。
- 因此"避免长事务"在 MVCC 数据库中不是风格建议,而是架构约束。
7.2 辅助索引的差异
与聚簇索引不同,辅助索引记录不包含 DB_TRX_ID 和 DB_ROLL_PTR 两个隐藏列。当通过辅助索引进行一致性读时,如果发现索引页上的记录可能已被更新,InnoDB 需要回表到聚集索引,利用聚集索引记录的版本链信息判断可见性并构建正确的数据版本。
7.3 MVCC 仅在 RC / RR 下生效
| 隔离级别 | 是否使用 MVCC |
|---|---|
READ UNCOMMITTED |
否,直接读最新版本 |
READ COMMITTED |
是,每次 SELECT 生成新 Read View |
REPEATABLE READ |
是,首次 SELECT 生成并复用 Read View |
SERIALIZABLE |
否,靠锁实现 |
八、核心概念速查表
| 概念 | 一句话解释 |
|---|---|
DB_TRX_ID |
"裁判":记录最后修改该行的事务 ID,用于判断当前版本是否可见 |
DB_ROLL_PTR |
"向导":7 字节编码地址,指向 Undo Log 中上一个版本的位置 |
| Undo Log | 存储旧版本数据,UPDATE/DELETE 的 Undo 需服务 MVCC,INSERT 的 Undo 提交后即可清理 |
| 版本链 | 由 DB_ROLL_PTR 串联的从最新到最旧的历史版本序列 |
| Read View | 可见性规则集,包含 m_ids、min_trx_id、max_trx_id、creator_trx_id |
| 快照读 | 普通 SELECT,走 MVCC,不加锁 |
| 当前读 | SELECT ... FOR UPDATE、UPDATE、DELETE,读最新版本并加锁 |
| Purge | 后台线程,确认无快照需要后清理旧 Undo 版本 |
九、总结
InnoDB 的 MVCC 通过 隐藏列 + Undo Log 版本链 + Read View 三者协同,实现了"读不阻塞写、写不阻塞读":
- 写操作 :将旧值写入 Undo Log,更新当前行的
DB_TRX_ID,DB_ROLL_PTR指向新 Undo 记录,形成版本链; - 读操作 :从最新版本开始,用
DB_TRX_ID与 Read View 比对判断可见性,不可见则顺DB_ROLL_PTR回溯,直到找到可见版本; - RC 与 RR 的差异:仅在于 Read View 的生成时机(每条 SELECT vs 首次 SELECT);
- 代价:长事务会阻塞 Purge 导致 Undo 膨胀;快照隔离存在写偏斜的理论缺口;写写冲突仍需锁解决。