跳到主要内容
约 9 分钟5 张卡

MVCC

MVCC 通过历史版本减少 InnoDB 普通读取与写入之间的阻塞。本文讲解 MySQL 8.0 RC/RR 的版本链与 Read View。内容还包括混合读写的可见性。

1. 面试核心​

先记住这几条,再读正文

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会话 BA 的读取结果
1SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION;—尚未创建普通一致性读快照
2—UPDATE mvcc_demo SET amount=110 WHERE id=1;—
3SELECT amount FROM mvcc_demo WHERE id=1;—110,首次一致性读建立快照
4—UPDATE mvcc_demo SET amount=120 WHERE id=1;—
5再执行第 3 步查询—仍为 110
6SELECT amount FROM mvcc_demo WHERE id=1 FOR UPDATE;—120,锁定读访问当前已提交记录
7COMMIT; 后再次普通查询—120,旧事务已结束

若 A 改为 RC,第 5 步会读到 120。第 6 步不会把 RR 原有 Read View 更新成新快照;当前读可以看到新值,后续普通查询仍可读取旧快照。

再考虑快照建立时,一个修改事务尚未提交。即使它稍后提交,RR 也不会把该版本纳入旧 Read View。可见性判断依据建快照时的事务状态。

9.1 混合读写与新行可见性​

另一事务在快照后插入的行,可以被 RR 事务更新。更新后,普通 SELECT 可以看到自己生成的新版本。使用前述 mvcc_demo,先确保表中只有 (1, 100)。两个会话均须结束上一轮事务,B 使用自动提交:

步骤会话 A(RR)会话 BA 的结果
1START TRANSACTION; SELECT * FROM mvcc_demo WHERE id >= 2;—空,建立快照
2—INSERT INTO mvcc_demo VALUES (2, 200);B 提交新行
3SELECT * FROM mvcc_demo WHERE id >= 2;—仍为空
4UPDATE mvcc_demo SET amount=amount+1 WHERE id=2;—影响 1 行
5SELECT * FROM mvcc_demo WHERE id >= 2;—返回 (2, 201)
6ROLLBACK;—撤销 A 的更新,B 插入的行仍保留

第 4 步 UPDATE 是当前读,可以访问 B 已提交的新行。它将该行改为 A 自己的版本。第 5 步适用“自己的修改可见”规则,因而返回新行。原 Read View 保持不变。混合快照读与当前读时,这种现象常被称为“幻读”。纯快照读在没有自身写入时,仍维持原有可见范围。

10. 常见问题​

10.1 MVCC 与锁的分工​

MVCC 主要减少普通读取与写入之间的阻塞。写写冲突、锁定读、唯一约束和外键检查等操作仍需要锁。事务能看到哪个版本与能否修改当前记录是两个问题。

10.2 undo log 的用途​

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 的事务级快照、双会话可见性推演。

参考回答

可以。

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 仍保持不变。这种新增可见行来自自己的写入。

12. 参考资料​