MVCC 多版本并发控制
问题
什么是 MVCC?InnoDB 如何通过 MVCC 实现非阻塞读?Read View 的可见性判断规则是什么?
答案
一、MVCC 概述
MVCC(Multi-Version Concurrency Control)是 InnoDB 实现 读-写不阻塞 的核心机制。它让普通的 SELECT 语句不需要加锁就能读取到一个一致性的数据快照。
| 操作 | 是否需要锁 |
|---|---|
快照读 SELECT | ❌ 不需要,走 MVCC |
当前读 SELECT FOR UPDATE | ✅ 需要加锁 |
INSERT / UPDATE / DELETE | ✅ 需要加锁 |
核心价值
没有 MVCC,读写必须串行化。有了 MVCC,读不阻塞写,写不阻塞读,极大提升了并发性能。
二、MVCC 的实现基础
MVCC 依赖三个组件:
1. 隐藏字段
InnoDB 为每行数据自动添加三个隐藏列:
| 隐藏列 | 大小 | 说明 |
|---|---|---|
| DB_TRX_ID | 6B | 最后修改该行的事务 ID |
| DB_ROLL_PTR | 7B | 回滚指针,指向 undo log 中的旧版本 |
| DB_ROW_ID | 6B | 自动生成的行 ID(无主键时) |
2. undo log 版本链
每次 UPDATE 操作,旧数据不会被直接覆盖,而是被写入 undo log,通过 DB_ROLL_PTR 串成一条 版本链:
读取数据时,根据可见性规则,沿着版本链找到对当前事务「可见」的那个版本。
3. Read View(读视图)
Read View 是事务在做快照读时生成的一个「快照」,包含以下信息:
| 字段 | 含义 |
|---|---|
| m_ids | 创建 Read View 时,所有 活跃(未提交)事务 的 ID 列表 |
| min_trx_id | m_ids 中的最小值 |
| max_trx_id | 系统即将分配的下一个事务 ID(即当前最大事务 ID + 1) |
| creator_trx_id | 创建该 Read View 的事务 ID |
三、可见性判断规则(最核心)
对于版本链中的每个版本,检查它的 DB_TRX_ID(记为 trx_id):
规则总结(按优先级):
| 条件 | 结论 | 原因 |
|---|---|---|
trx_id == creator_trx_id | ✅ 可见 | 是自己修改的 |
trx_id < min_trx_id | ✅ 可见 | 在 Read View 创建前已提交 |
trx_id >= max_trx_id | ❌ 不可见 | 在 Read View 创建后才开始 |
trx_id 在 m_ids 中 | ❌ 不可见 | 创建 Read View 时还未提交 |
trx_id 不在 m_ids 中 | ✅ 可见 | 创建 Read View 时已提交 |
如果当前版本不可见,就沿着版本链往前找,直到找到可见的版本。
四、RC 和 RR 的 Read View 差异
| 隔离级别 | Read View 创建时机 | 效果 |
|---|---|---|
| READ COMMITTED | 每条 SELECT 都创建新的 Read View | 能看到其他事务最新提交的数据 |
| REPEATABLE READ | 事务第一条 SELECT 时创建,后续复用 | 始终看到同一个快照 |
详细示例
假设有一行 id=1, name='张三',当前 TRX_ID=100。
时间线:
T1: 事务 A (TRX_ID=200) 开始
T2: 事务 B (TRX_ID=300) 开始
T3: 事务 A 执行 SELECT → 创建 Read View (m_ids=[200,300], min=200, max=301)
T4: 事务 B 执行 UPDATE name='李四' (TRX_ID=300 写入版本链)
T5: 事务 B COMMIT
T6: 事务 A 再次 SELECT
RR 级别:
- T3 创建 Read View:m_ids=[200,300]
- T6 复用 T3 的 Read View
- 版本链:name='李四'(trx_id=300) → name='张三'(trx_id=100)
- 检查 trx_id=300:在 m_ids 中 → 不可见
- 检查 trx_id=100:< min_trx_id(200) → 可见
- 结果:读到 name='张三'(快照一致)
RC 级别:
- T6 创建新的 Read View:m_ids=[200](事务 B 已提交,不在活跃列表中)
- 检查 trx_id=300:不在 m_ids 中且 < max_trx_id → 可见
- 结果:读到 name='李四'(看到已提交的最新数据)
五、快照读 vs 当前读
| 类型 | SQL | 机制 |
|---|---|---|
| 快照读 | SELECT ... | MVCC Read View,不加锁 |
| 当前读 | SELECT ... FOR UPDATESELECT ... FOR SHAREINSERT / UPDATE / DELETE | 读取最新已提交数据,加锁 |
混用快照读和当前读的陷阱
在 RR 级别下:
BEGIN;
SELECT * FROM users WHERE id = 5; -- 快照读,看不到其他事务的修改
UPDATE users SET age = age + 1 WHERE id = 5; -- 当前读!基于最新数据修改
SELECT * FROM users WHERE id = 5; -- 快照读,但能看到自己的修改
这可能导致困惑:第一次读和修改后的读结果不一致。
常见面试问题
Q1: MVCC 能完全替代锁吗?
答案:
不能。MVCC 只解决了 读–写 并发问题(快照读不阻塞写)。写–写 并发仍然需要锁来保护。INSERT/UPDATE/DELETE 都是当前读,必须加锁。
Q2: undo log 什么时候可以被删除?
答案:
当没有任何活跃的 Read View 需要访问某个 undo log 版本时,它就可以被 purge 线程回收。
这就是 长事务的危害:长事务持有的 Read View 会阻止旧版本 undo log 被回收,导致 undo 表空间膨胀。
Q3: MVCC 和 Read View 的关系?
答案:
- MVCC 是整体机制(版本链 + 可见性判断)
- Read View 是 MVCC 中用于判断可见性的 快照信息
- Read View 记录了创建时刻的活跃事务列表
- 通过 Read View 遍历版本链,找到当前事务应该看到的版本