MVCC 中的可见性判断规则
总结
本文深入解析 MVCC 中的可见性判断规则,并结合示例进行说明。
简介
在支持 MVCC(多版本并发控制) 的数据库系统中(如 MySQL 的 InnoDB 引擎),每行数据可能有多个版本,每个版本都记录了修改它的事务 ID(DB_TRX_ID
)。为了实现事务隔离性,数据库会为每个一致性读操作创建一个 Read View(即:一致性读视图)。
可见性判断规则是 MVCC 的核心逻辑之一,事务根据当前的 Read View,判断某个数据版本是否对自己可见。这不仅决定了事务能否读取到该数据,还影响 事务隔离级别 行为(如可重复读和读已提交)。
MVCC 可见性判断的规则
前置知识
MySQL InnoDB 引擎中每一行数据都隐藏了两个字段:
DB_TRX_ID
:创建或最后一次修改该行数据的事务 ID。DB_ROLL_PTR
:回滚指针,指向该行的上一个版本在 Undo Log 中的位置。
Read View 主要包含以下关键字段:
m_ids
:当前活跃事务的 ID 列表(即尚未提交的事务)。min_trx_id
:当前活跃事务中最小的事务 ID。max_trx_id
:下一个将要分配的事务 ID(即当前已分配的最大事务 ID + 1)。creator_trx_id
:创建视图的事务 ID。
Read View 创建时机:
- READ COMMITTED(RC):每次快照读生成新的一致性视图。
- REPEATABLE READ(RR):整个事务期间使用同一个一致性视图。
可见性判断规则(RC、RR 隔离级别)
当事务 A(如:TRX_ID = 101)读取某一行数据时,它会获取该行当前版本的 DB_TRX_ID
,并根据当前的 Read View 中的信息进行可见性判断。
1. 当前事务自己修改的数据,始终可见
- 条件:
DB_TRX_ID 等于当前事务 ID
- 结论:✅ 可见
- 说明:即使事务尚未提交,也能看到自己修改的数据。
- 示例:事务 A 修改某行数据但未提交 → 再次读取时能看到自己的修改(无论 RC 还是 RR)
2. 在 Read View 创建之前已提交的事务修改的数据,可见
- 条件:
DB_TRX_ID 小于 min_trx_id
- 结论:✅ 可见
- 说明:这些事务在 Read View 创建时已经提交。
- 示例:
- 在 RC 中,每次读取都生成新的 Read View,因此这个“已提交事务”是相对于当前这次读操作的 Read View 而言的。
- 在 RR 中,这个“已提交事务”是相对于事务第一次读操作的 Read View 而言的。
3. Read View 创建之后才开始的事务修改的数据,当前 Read View 不可见
- 条件:
DB_TRX_ID 大于等于 max_trx_id
- 结论:当前 Read View 不可见
- 说明:这些事务是在 Read View 创建之后才开始的,不属于当前一致性视图。
- 示例:
- 事务 A(RC)第一次读时事务 105 未提交 → 不可见。事务 105 提交后,事务 A 第二次读 → 生成新的 Read View,可以看到事务 105 的修改。
- 事务 A(RR)第一次读时事务 105 未提交 → 不可见。事务 105 提交后,事务 A 第二次读 → 仍不可见(Read View 未变)。
4. 介于 min_trx_id 和 max_trx_id 之间的事务修改的数据
- 条件:
min_trx_id ≤ DB_TRX_ID < max_trx_id
- 判断方式:检查
DB_TRX_ID
是否在活跃事务列表m_ids
中- 在列表中 → 事务尚未提交,数据不可见
- 不在列表中 → 事务已提交,数据可见
- 示例:
- 事务 A(RC)第一次读时,事务 102 正在运行(活跃)→ 不可见。
- 事务 102 提交后,事务 A 第二次读 → 活跃事务列表中不含 102 → 可见。
5. 如果当前版本不可见,则追溯更早的版本
- 操作:沿着版本链(通过
DB_ROLL_PTR
)向前查找,重复以上判断 - 目的:找到一个对当前事务可见的数据版本
- 结束条件:找到可见版本,或到达版本链的末尾(到达末尾未能找到就返回空)
示例:版本链回溯与可见性判断详细过程
以 RR 隔离级别示例,假设当前事务 A 的事务 ID 是 TRX_ID = 101
,它执行了一个一致性读操作,此时 Read View 中的信息如下:
m_ids = [100, 102]
min_trx_id = 100
max_trx_id = 103
creator_trx_id=101
。
假设此时 Undo Log 有以下几个版本的数据:
数据版本 | DB_TRX_ID | 是否可见 | 判断依据 | DB_ROLL_PTR(回滚指针:指向前一版本) |
---|---|---|---|---|
版本 5 | 103 | ❌ 不可见 | 大于等于 max_trx_id | → 版本 4 |
版本 4 | 102 | ❌ 不可见 | 在活跃事务列表中 | → 版本 3 |
版本 3 | 101 | ✅ 可见 | 是当前事务自己修改的 | → 版本 2 |
版本 2 | 100 | ❌ 不可见 | 在活跃事务列表中 | → 版本 1 |
版本 1 | 99 | ✅ 可见 | 小于 min_trx_id,已提交 | → NULL(链表末尾) |
事务 A 会从当前版本(最新版本:版本 5)开始,沿着版本链逐级回溯,直到找到一个对自己可见的版本或到达链表末尾。。
第一步:判断版本 5(DB_TRX_ID = 103)
103 ≥ max_trx_id(103)
→ 条件成立- 结论:❌ 不可见
- 操作:通过
DB_ROLL_PTR
指针,回溯到前一个版本(版本 4)
第二步:判断版本 4(DB_TRX_ID = 102)
100 ≤ 102 < 103
→ 在 min 和 max 之间- 查看是否在活跃事务列表
m_ids = [100, 102]
中 → 存在 - 结论:❌ 不可见
- 操作:继续回溯到前一个版本(版本 3)
第三步:判断版本 3(DB_TRX_ID = 101)
DB_TRX_ID = 当前事务 ID(101)
- 结论:✅ 可见
- 操作:找到可见版本,读取该行数据,回溯结束