MySQL InnoDB MVCC 多版本并发控制实现原理
核心问题
在没有 MVCC 的情况下,为了保证事务隔离性(如可重复读),通常使用锁机制。这会导致'读 - 写'和'写 - 读'冲突。MVCC 通过创建数据的历史版本来解决这一问题。
核心思想: 为每行数据维护多个历史版本。当事务读取数据时,看到的是在它开始之前已提交的某个一致性快照,不受其他事务修改影响。
实现基石
MVCC 的实现依赖于三个核心组件:隐藏字段、Undo Log 和 Read View。
1. 隐藏字段
InnoDB 为每一行数据添加了三个系统隐藏字段:
DB_TRX_ID (6 字节):事务 ID,表示最后一次插入或更新该行的事务 ID。
DB_ROLL_PTR (7 字节):回滚指针,指向该行数据的上一个历史版本,存储在 Undo Log 中。
DB_ROW_ID (6 字节):行 ID,单调递增。若无主键,InnoDB 基于此生成聚簇索引。
(图:隐藏字段结构)
2. Undo Log
Undo Log 存储了数据行的历史版本,是 MVCC 的关键。
工作原理:
- UPDATE / DELETE: 先将当前版本复制到 Undo Log,新版本的
DB_ROLL_PTR 指向旧版本。然后修改表中数据。
- INSERT: 新插入的数据对之前事务不可见,Undo Log 主要用于回滚。
通过 DB_ROLL_PTR,一行数据的所有历史版本被串联成链表,称为版本链,存放在 Undo Log 中。
(图:版本链结构)
3. Read View(读视图)
Read View 是事务在进行快照读操作时产生的,定义了当前事务能看到哪些版本的数据。
主要属性:
m_ids:生成 Read View 时,系统中活跃的未提交事务 ID 列表。
min_trx_id:m_ids 中的最小值。
max_trx_id:生成 Read View 时,系统应分配给下一个事务的 ID。
creator_trx_id:创建该 Read View 的事务 ID。
可见性算法
当事务执行快照读时,遍历版本链并利用 Read View 判断版本可见性。假设版本对应事务 ID 为 trx_id:
- 若
trx_id == creator_trx_id:当前事务自己修改的,可见。
- 若
trx_id < min_trx_id:在 Read View 创建前已提交,可见。
- 若
trx_id >= max_trx_id:在 Read View 创建后开启,不可见。
- 若
min_trx_id <= trx_id < max_trx_id:检查 trx_id 是否在 m_ids 中。若在(活跃),不可见;若不在(已提交),可见。
(图:可见性判断流程图)
不同隔离级别下的表现
MVCC 主要在 READ COMMITTED 和 REPEATABLE READ 下工作。
1. REPEATABLE READ(可重复读)
- 特性: 同一事务中第一次快照读创建 Read View,后续复用该视图。
- 效果: 无论其他事务如何提交,本事务看到的数据快照始终一致。
2. READ COMMITTED(读已提交)
- 特性: 每次快照读都会生成一个新的独立 Read View。
- 效果: 每次都能看到本次查询开始前已提交的所有事务修改。
总结
- 每行数据有隐藏字段
DB_TRX_ID 和 DB_ROLL_PTR。
- 修改操作在 Undo Log 中创建历史版本,形成版本链。
- 事务在快照读时生成 Read View(RC 每次生成,RR 第一次生成)。
- 通过可见性算法遍历版本链,找到对当前事务可见的版本。
补充说明
- 快照读 vs 当前读
- 快照读:普通 SELECT,基于 MVCC 读取历史版本,不加锁。
- 当前读:特殊 SELECT(如 FOR UPDATE)及 INSERT/UPDATE/DELETE,读取最新版本并加锁。
- Purge 操作
- 后台 Purge 线程清理不再被任何事务 Read View 需要的旧版本数据,释放空间。