跳到主要内容

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_ID6B最后修改该行的事务 ID
DB_ROLL_PTR7B回滚指针,指向 undo log 中的旧版本
DB_ROW_ID6B自动生成的行 ID(无主键时)

2. undo log 版本链

每次 UPDATE 操作,旧数据不会被直接覆盖,而是被写入 undo log,通过 DB_ROLL_PTR 串成一条 版本链

读取数据时,根据可见性规则,沿着版本链找到对当前事务「可见」的那个版本。

3. Read View(读视图)

Read View 是事务在做快照读时生成的一个「快照」,包含以下信息:

字段含义
m_ids创建 Read View 时,所有 活跃(未提交)事务 的 ID 列表
min_trx_idm_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_idm_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 UPDATE
SELECT ... FOR SHARE
INSERT / 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 遍历版本链,找到当前事务应该看到的版本

相关链接