跳到主要内容

MVCC 多版本并发控制

问题

什么是 MVCC?它是如何实现的?Read View 的创建时机与可见性判断规则是什么?

答案

什么是 MVCC

MVCC(Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 实现事务隔离性的核心机制。它通过为每一行数据维护多个版本,使得读操作不需要加锁就能获取一致性快照,实现了读写不阻塞

操作组合是否冲突说明
读 - 读❌ 不冲突都不加锁
读 - 写❌ 不冲突读走 MVCC 快照,写走当前版本
写 - 写✅ 冲突通过行锁串行化
MVCC 只在 RC 和 RR 级别下工作
  • READ UNCOMMITTED 直接读最新值,不使用 MVCC
  • SERIALIZABLE 所有读都加锁,不使用 MVCC

MVCC 的三大组件

1. 隐藏字段

InnoDB 为每一行数据自动添加 3 个隐藏字段:

字段大小说明
DB_TRX_ID6 字节最近修改该行的事务 ID
DB_ROLL_PTR7 字节回滚指针,指向 undo log 中该行的上一个版本
DB_ROW_ID6 字节隐含自增 ID(无主键时自动生成)

2. Undo Log 版本链

每次修改一行数据,旧版本都会被写入 undo log,通过 DB_ROLL_PTR 串成一条版本链

执行 SELECT 时,InnoDB 沿着版本链从新到旧查找,找到第一个对当前事务可见的版本返回。

3. Read View(读视图)

Read View 是事务在执行快照读时创建的一个数据快照,用于判断版本链中哪个版本对当前事务可见。

Read View 包含 4 个关键字段:

字段说明
m_ids创建 Read View 时活跃的事务 ID 列表(已开始但未提交的事务)
min_trx_idm_ids 中的最小值(活跃事务中最小的 ID)
max_trx_id创建 Read View 时系统即将分配的下一个事务 ID(最大活跃 ID + 1)
creator_trx_id创建该 Read View 的事务 ID

可见性判断规则

对于版本链中的每一个版本,其 trx_id 按以下规则判断可见性:

简化记忆:

  1. 自己修改的 → 可见
  2. 事务 ID < min_trx_id → 创建 Read View 前已提交 → 可见
  3. 事务 ID >= max_trx_id → 创建 Read View 后才开始 → 不可见
  4. 在 m_ids 中 → 创建 Read View 时还未提交 → 不可见
  5. 不在 m_ids 中且 < max_trx_id → 创建 Read View 前已提交 → 可见

Read View 创建时机

RC 和 RR 的核心区别就在于 Read View 的创建时机:

隔离级别Read View 创建时机效果
RC(读已提交)每次 SELECT 都创建新的 Read View每次读都能看到最新提交的数据
RR(可重复读)事务第一次 SELECT 时创建,后续复用整个事务看到的都是同一个快照

完整示例

假设有一行数据 id=1, name='张三',初始 trx_id=100(已提交)。

RC 级别下的区别

如果隔离级别是 RC,第二次 SELECT 时会重新创建 Read View,此时事务 B 已提交,m_ids 中不再包含 300,所以 name='李四' 变为可见,结果为 name='李四'

MVCC 与锁的配合

操作使用机制说明
普通 SELECTMVCC(快照读)不加锁,读版本链中可见版本
SELECT ... FOR UPDATE当前读 + X 锁读最新已提交版本,加排他锁
SELECT ... FOR SHARE当前读 + S 锁读最新已提交版本,加共享锁
INSERT / UPDATE / DELETE当前读 + X 锁操作最新版本,加排他锁

MVCC 解决了快照读的隔离问题,锁解决了当前读的隔离问题。两者配合实现了 InnoDB 的完整事务隔离。

关于锁的详细说明,参见 MySQL 锁机制

Undo Log 的清理

版本链不能无限增长,InnoDB 有一个 Purge 线程 负责清理不再需要的 undo log:

  • 当没有任何活跃事务需要访问某个旧版本时,该版本才能被清理
  • 这就是长事务导致 undo log 膨胀的原因:长事务的 Read View 持有较老的 min_trx_id,导致大量旧版本无法清理

常见面试问题

Q1: MVCC 是什么?解决了什么问题?

答案

MVCC(多版本并发控制)是一种并发控制技术,InnoDB 通过为数据行维护多个版本(版本链),让读操作可以读取历史版本,从而实现:

  1. 读写不阻塞:读操作不需要加锁,不会被写操作阻塞,提高并发性能
  2. 一致性读:事务在执行期间看到的数据是一致的快照,不受其他事务影响

MVCC 通过三个组件实现:隐藏字段(trx_id、roll_ptr)、undo log 版本链Read View

Q2: RC 和 RR 隔离级别下 MVCC 的区别是什么?

答案

唯一的区别是 Read View 的创建时机

  • RC(读已提交)每次 SELECT 都创建新的 Read View,所以能看到其他事务在两次读之间提交的数据,导致不可重复读
  • RR(可重复读)事务第一次 SELECT 时创建 Read View,整个事务复用同一个 Read View,所以看到的始终是同一个快照,实现了可重复读

这也是为什么说 RR 的性能开销其实不比 RC 大多少——区别仅在于 Read View 的创建频率。

Q3: MVCC 能完全解决幻读吗?

答案

不能完全解决。MVCC 只能解决快照读场景下的幻读,无法解决当前读的幻读。

在 RR 级别下:

  • 快照读(普通 SELECT):通过 MVCC 看不到其他事务新插入的行 → 没有幻读 ✅
  • 当前读(SELECT FOR UPDATE / INSERT / UPDATE / DELETE):需要通过 Next-Key Lock(间隙锁) 来阻止其他事务在范围内插入新行 → 通过锁解决幻读 ✅

但如果事务中先快照读后当前读,可能出现逻辑上的幻象。所以严格说,InnoDB 通过 MVCC + Next-Key Lock 组合来解决幻读。

Q4: Read View 的可见性判断规则是什么?

答案

对版本链中某个版本的 trx_id 进行判断:

  1. trx_id == creator_trx_id可见(自己的修改)
  2. trx_id < min_trx_id可见(在 Read View 创建前已提交)
  3. trx_id >= max_trx_id不可见(在 Read View 创建后才产生的事务)
  4. min_trx_id <= trx_id < max_trx_id
    • m_ids 中 → 不可见(创建 Read View 时尚未提交)
    • 不在 m_ids 中 → 可见(创建 Read View 前已提交)

如果当前版本不可见,就顺着 roll_ptr 找版本链中的上一个版本,重复判断直到找到可见版本。

Q5: 为什么长事务会导致 undo log 膨胀?

答案

长事务持有的 Read View 中 min_trx_id 很小(等于事务开始时的活跃事务最小 ID),这意味着所有 trx_id >= min_trx_id 的旧版本都不能被 Purge 线程清理,因为长事务可能还需要读取这些旧版本。

后果:

  1. undo log 持续增长,占用大量磁盘空间
  2. 读性能下降:版本链越长,快照读需要遍历的版本越多
  3. 回滚段空间不足:可能导致 tread is too old 错误

因此生产环境应监控长事务并及时处理:

-- 查找超过 60 秒的长事务
SELECT * FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;

相关链接