MVCC
MVCC 通过历史版本减少 InnoDB 普通读取与写入之间的阻塞。本文讲解 MySQL 8.0 RC/RR 的版本链与 Read View。内容还包括混合读写的可见性。
1. 面试核心
先记住这几条,再读正文- 版本可见性。 MVCC 通过事务信息、undo 历史版本与 Read View 判断可见性。写写冲突仍需要锁。卡 · 行版本与 undo 链
- 快照建立时机。 RC 每次一致性读创建 Read View。RR 复用首次一致性读的 Read View。
WITH CONSISTENT SNAPSHOT可在 RR 事务启动时建立快照。卡 · RR 的事务级快照 - 版本链与自身修改。 当前事务自己的修改可见。其他事务的修改若对快照不可见,InnoDB 就沿版本链寻找旧值。卡 · 行版本与 undo 链
- 快照读与当前读。 RR 的普通 SELECT 使用旧快照。FOR UPDATE 与 UPDATE 使用当前记录。事务自己的修改对后续 SELECT 可见。 卡 · 快照读与当前读卡 · 混合读写与新行可见性
- 长快照成本。 活跃 Read View 需要保留旧版本,会延迟 purge 清理。导出分批提交会切换快照。卡 · 长快照与版本保留
2. 行版本与 undo 链
InnoDB 为聚簇索引记录保存事务信息。它记录创建或最后修改该版本的事务。回滚指针则关联 undo log 中的旧版本。关键字段是 DB_TRX_ID 和 DB_ROLL_PTR。更新一行时,新值写入当前记录。旧值可以通过 undo 信息重建。
活跃事务都不再需要旧版本后,purge 才能清理相应 undo 记录。长时间持有快照的事务会延迟清理。
3. Read View 与版本可见性
Read View 会记录创建快照时尚未完成的事务范围。判断一个行版本是否可见时,可以按以下原则理解:
- 当前事务自己的修改可见。
- 快照建立前已经提交的事务版本可见。
- 快照建立时仍活跃的事务版本不可见。
- 建快照时尚未分配事务 ID 的后续写事务,其版本不可见。
若最新版本不可见,InnoDB 沿 undo 版本链查找更早的版本。找到可见版本或确认记录不存在后,查找才结束。
ReadView::changes_visible 按下面的顺序判断可见性。先单独处理当前事务,再判断其他事务的版本:
| 条件 | 可见性 |
|---|---|
版本事务 ID 等于创建者 m_creator_trx_id | 自己的修改可见 |
小于 m_up_limit_id | 早于活跃事务下界,可见 |
大于等于 m_low_limit_id | 超出快照的事务 ID 上界,不可见 |
| 位于两个边界之间 | 在建快照时的活跃读写事务集合 m_ids 中则不可见,否则可见 |
例如创建者为 100,其他活跃事务为 101、103,上界为 105。这时,99 和 102 可见,101 和 105 不可见。101 后来提交也不会修改这张旧视图。字段名中的 up/low 容易与数值上下界的直觉相反。面试时应说明比较关系和活跃集合,避免只背字段名。
4. RC 的语句级快照
在 Read Committed 下,每次普通一致性读都会取得新的 Read View。同一事务内的多次读取也遵循这一规则。若另一事务在两次查询之间提交,后一次查询就能看到新结果。这就是不可重复读。
RC 能更快结束对旧版本的读取需求,并减少部分锁竞争。代价是事务内多次查询不再共享同一个时间点。
5. RR 的事务级快照
在 Repeatable Read 下,事务的第一次普通一致性读建立快照。后续一致性读继续使用它。因此,其他事务之后提交的新增或修改不会出现在这些快照读中。
START TRANSACTION;
SELECT * FROM orders WHERE id = 100; -- 在这里建立快照
普通 START TRANSACTION 将 Read View 的创建推迟到首次一致性读。若需要立即建立一致快照,在 RR 下可使用 START TRANSACTION WITH CONSISTENT SNAPSHOT。MySQL 8.0 只在 RR 下支持这项快照语义。其他隔离级别会忽略该子句并产生警告。
同一事务自己的写入仍然可见。事务可以既读取旧快照,又修改最新状态。这时,结果会包含自己的新版本和其他行的旧版本。具体例子见混合读写与新行可见性。
6. 快照读与当前读
RC/RR 下普通 SELECT 使用快照读,从版本链中寻找可见版本。以下操作需要读取当前版本,并参与并发控制:
SELECT ... FOR SHARE、SELECT ... FOR UPDATE、UPDATE 和 DELETE 都属于这类操作。
当前读访问当前记录,并通过锁协调冲突;具体等待规则见 InnoDB 锁。余额扣减业务如果先检查、再修改,就要保护检查结果。业务可使用锁定读、条件更新或其他原子约束。
7. 快照可见性与范围锁
普通一致性读复用同一个 Read View 时,可见范围保持稳定。快照建立后提交的新行,对后续快照查询不可见。
锁定读和写操作会保护记录及间隙,以约束并发插入。具体机制见 InnoDB 锁。
8. 长快照与版本保留
只读事务也能长时间持有旧 Read View。其他事务持续更新时,purge 必须保留该视图所需的 undo 历史。这会增加存储与旧版本读取成本。
批处理、导出和管理脚本应设置运行时限。还应监控活跃事务的持续时间与 undo 历史。允许跨批次看到新数据时可以分批提交;若全量导出必须来自同一快照,就要避免分批提交。此时应控制快照持续时间与负载,或使用专门的备份方案。不要让一个无人关注的会话长期保持事务打开。
9. 双会话可见性推演
先创建独立的练习表:
CREATE TABLE mvcc_demo (id INT PRIMARY KEY, amount INT) ENGINE=InnoDB;
INSERT INTO mvcc_demo VALUES (1, 100);
以下步骤按表格从上到下执行,B 使用默认自动提交,A 显式设为 RR:
| 步骤 | 会话 A | 会话 B | A 的读取结果 |
|---|---|---|---|
| 1 | SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; | — | 尚未创建普通一致性读快照 |
| 2 | — | UPDATE mvcc_demo SET amount=110 WHERE id=1; | — |
| 3 | SELECT amount FROM mvcc_demo WHERE id=1; | — | 110,首次一致性读建立快照 |
| 4 | — | UPDATE mvcc_demo SET amount=120 WHERE id=1; | — |
| 5 | 再执行第 3 步查询 | — | 仍为 110 |
| 6 | SELECT amount FROM mvcc_demo WHERE id=1 FOR UPDATE; | — | 120,锁定读访问当前已提交记录 |
| 7 | COMMIT; 后再次普通查询 | — | 120,旧事务已结束 |
若 A 改为 RC,第 5 步会读到 120。第 6 步不会把 RR 原有 Read View 更新成新快照;当前读可以看到新值,后续普通查询仍可读取旧快照。
再考虑快照建立时,一个修改事务尚未提交。即使它稍后提交,RR 也不会把该版本纳入旧 Read View。可见性判断依据建快照时的事务状态。
9.1 混合读写与新行可见性
另一事务在快照后插入的行,可以被 RR 事务更新。更新后,普通 SELECT 可以看到自己生成的新版本。使用前述 mvcc_demo,先确保表中只有 (1, 100)。两个会话均须结束上一轮事务,B 使用自动提交:
| 步骤 | 会话 A(RR) | 会话 B | A 的结果 |
|---|---|---|---|
| 1 | START TRANSACTION; SELECT * FROM mvcc_demo WHERE id >= 2; | — | 空,建立快照 |
| 2 | — | INSERT INTO mvcc_demo VALUES (2, 200); | B 提交新行 |
| 3 | SELECT * FROM mvcc_demo WHERE id >= 2; | — | 仍为空 |
| 4 | UPDATE mvcc_demo SET amount=amount+1 WHERE id=2; | — | 影响 1 行 |
| 5 | SELECT * FROM mvcc_demo WHERE id >= 2; | — | 返回 (2, 201) |
| 6 | ROLLBACK; | — | 撤销 A 的更新,B 插入的行仍保留 |
第 4 步 UPDATE 是当前读,可以访问 B 已提交的新行。它将该行改为 A 自己的版本。第 5 步适用“自己的修改可见”规则,因而返回新行。原 Read View 保持不变。混合快照读与当前读时,这种现象常被称为“幻读”。纯快照读在没有自身写入时,仍维持原有可见范围。
10. 常见问题
10.1 MVCC 与锁的分工
MVCC 主要减少普通读取与写入之间的阻塞。写写冲突、锁定读、唯一约束和外键检查等操作仍需要锁。事务能看到哪个版本与能否修改当前记录是两个问题。
10.2 undo log 的用途
undo 信息用于回滚未提交修改。它也用于构造一致性读需要的历史版本。活跃快照持续时间会影响这些历史版本何时可以被清理。
11 · 面试题卡片
11.1 InnoDB 的 MVCC 和 Read View 如何实现可重复读
出现公司:美团
考察重点
- 理解事务信息、undo 版本链与 Read View 的分工。
- 理解Read Committed 和 Repeatable Read 的快照时机。
- 理解快照读、当前读和 next-key lock 的边界。
相关内容:行版本与 undo 链、Read View 与版本可见性、RR 的事务级快照。
参考回答
InnoDB 用 undo 版本链保存历史值,用 Read View 判断哪些版本可见;RR 事务内复用 Read View,实现可重复读。
一致性读按快照建立时的事务状态判断可见性,最新版本不可见时,沿 undo 链寻找更早版本。RC 每次一致性读创建新快照,RR 则复用首次一致性读的快照,因此后者保持相同的外部版本可见范围。
本事务自己的修改仍可见。锁定读和更新使用当前记录,写入冲突由索引锁控制,不按旧快照直接决定修改结果。
11.2 执行 BEGIN 后,另一个事务提交的修改还能被第一次查询看到吗?
出现公司:暂无已核验的公开来源(本站自测)
考察重点
- 理解RR 快照建立时间。
- 理解RC 与 WITH CONSISTENT SNAPSHOT 的区别。
参考回答
可以。
RR 的普通显式事务在首次一致性读时创建 Read View。因此,查询能看到 BEGIN 之后、该次读取之前提交的值。RR 的 WITH CONSISTENT SNAPSHOT 会提前建立快照;RC 则为每次一致性读获取新快照。
11.3 同一个 RR 事务里,普通 SELECT 与 FOR UPDATE 为什么可能返回不同值?
出现公司:暂无已核验的公开来源(本站自测)
考察重点
- 理解快照可见性与当前读。
- 理解锁等待、自己的写入和幻读边界。
参考回答
普通 SELECT 按旧 Read View 取版本。
FOR UPDATE 锁定当前记录,能读到快照之后已提交的版本。有冲突的未提交写入时仍要等待。锁定读不会刷新原有快照,自己的更新又对自己可见。因此,混合读写的结果包含自己的新版本与其他行的旧版本。并发不变量应使用原子更新或明确的锁协议。
11.4 为什么只读事务也可能让 undo 历史持续增长?
出现公司:暂无已核验的公开来源(本站自测)
考察重点
- 理解旧版本保留条件。
- 理解导出一致性与分批提交的取舍。
相关内容:长快照与版本保留。
参考回答
只读事务持有 Read View,所需的旧版本必须保留。
其他事务持续更新时,就会累积 undo 历史。缩短快照或控制导出负载可以降低保留成本。分批提交允许使用新快照。业务接受跨批次不一致时,才能直接这样改。
11.5 RR 事务更新另一事务新插入的行后,为什么普通 SELECT 能看到它?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:当前读、自身写入可见性与 Read View 的稳定性。
相关内容:混合读写与新行可见性。
参考回答
UPDATE 使用当前读,修改了另一事务已提交的新行。
后续 SELECT 能看到本事务自己的修改,所以返回该行。以 id=2 为例,旧快照先返回空。UPDATE 将新插入的 200 改成 201,SELECT 随后返回 201。原 Read View 仍保持不变。这种新增可见行来自自己的写入。