深入浅出 MVCC —— 从零理解 MySQL 并发控制

一、从一个问题开始:为什么需要 MVCC?

想象一个简单的银行账户表,两个事务同时操作:

  • 事务 A:正在读取你的账户余额,准备显示在网页上。
  • 事务 B:同时给你的账户转账 100 元,正在修改余额。

在没有 MVCC 的传统锁机制下,为了保证数据一致性,要么让事务 A 等待事务 B 完成(加读锁),要么让事务 B 等待事务 A 完成(加写锁)。这种"读写互斥"的设计在高并发场景下会严重影响系统性能。

MVCC(Multi-Version Concurrency Control,多版本并发控制)正是为了解决这个问题而生。它允许数据库同时保留数据的多个版本,让读操作读取旧版本(快照),写操作创建新版本,从而实现 "读不阻塞写,写不阻塞读" 的理想并发模型。

二、MVCC 核心概念速览

2.1 什么是 MVCC?

MVCC 是一种并发控制方法,通过为数据行维护多个历史版本,使得事务可以在不加锁的情况下读取到一致的数据快照。InnoDB 存储引擎是 MySQL 中实现 MVCC 的典型代表。

2.2 两种读取方式

在理解 MVCC 前,必须先分清两种读取方式:

读取方式特点加锁情况典型语句
快照读读取数据的可见版本(可能是历史版本)不加锁普通 SELECT
当前读读取数据的最新版本加锁SELECT ... FOR UPDATEUPDATEDELETEINSERT

MVCC 主要作用于快照读,而当前读则需要依赖锁机制来保证一致性。

三、MVCC 的三大核心组件

MVCC 的实现依赖于三个关键组件:隐藏字段Undo Log 版本链Read View(读视图)

3.1 隐藏字段

InnoDB 为每行数据添加了三个隐藏字段:

隐藏字段大小含义
DB_TRX_ID6 字节最近修改该行的事务 ID
DB_ROLL_PTR7 字节回滚指针,指向 Undo Log 中的上一个版本
DB_ROW_ID6 字节隐藏主键(当表没有主键时自动生成)

3.2 Undo Log 版本链

当一行数据被更新时,InnoDB 不会直接覆盖原数据,而是:

  1. 将修改前的数据写入 Undo Log
  2. 更新当前行的 DB_TRX_ID 为新事务 ID
  3. DB_ROLL_PTR 指向上一个版本的 Undo Log 记录

这样,通过 DB_ROLL_PTR 指针,所有历史版本被串联成一个版本链,链头是最新版本,链尾是最早版本。

示例:假设初始版本由事务 1 插入,后续事务 2、3、4 依次更新,形成的版本链如下:

最新版 ← 版本4 (trx_id=4) ← 版本3 (trx_id=3) ← 版本2 (trx_id=2) ← 版本1 (trx_id=1)

3.3 Read View(读视图)

Read View 是快照读执行时判断数据可见性的依据,它记录了 Read View 生成时系统中活跃事务的信息。

Read View 包含四个重要字段:

字段含义
m_ids当前活跃事务的 ID 集合
min_trx_id最小活跃事务 ID
max_trx_id预分配事务 ID(当前最大事务 ID + 1)
creator_trx_id创建该 Read View 的事务 ID

四、可见性判断规则

当事务通过快照读访问一条记录时,会沿着版本链从新到旧依次判断每个版本是否可见。判断规则如下:

条件可见性说明
DB_TRX_ID == creator_trx_id✅ 可见当前事务修改的版本
DB_TRX_ID < min_trx_id✅ 可见修改事务在 Read View 生成前已提交
DB_TRX_ID > max_trx_id❌ 不可见修改事务在 Read View 生成后才开始
min_trx_id <= DB_TRX_ID <= max_trx_id需判断DB_TRX_ID 不在 m_ids 中则可见,否则不可见

规则总结:一个版本对当前事务可见,当且仅当修改该版本的事务在 Read View 生成时已经提交,或者就是当前事务本身

五、RC 与 RR 隔离级别的 MVCC 实现差异

5.1 两种隔离级别下的 Read View 生成策略

隔离级别Read View 生成策略
READ COMMITTED(RC)事务中每次执行快照读都会生成一个新的 Read View
REPEATABLE READ(RR)事务中第一次执行快照读时生成 Read View,后续复用

5.2 对比示例

假设有以下事务执行顺序:

  1. 事务 3、4、5 同时开始(均未提交)
  2. 事务 5 执行第一次查询
  3. 事务 3 提交
  4. 事务 5 执行第二次查询

RC 级别下

  • 第一次查询:Read View 中 m_ids={3,4,5},读不到事务 3 的修改
  • 第二次查询:生成新的 Read View,此时 m_ids={4,5},事务 3 已提交,因此可以读到事务 3 的修改 → 会产生不可重复读

RR 级别下

  • 第一次查询:Read View 中 m_ids={3,4,5}
  • 第二次查询:复用第一次的 Read View,m_ids 仍为 {3,4,5},事务 3 即使已提交也不可见 → 解决了不可重复读

六、MVCC 如何解决并发问题

6.1 解决脏读

脏读是指读到未提交事务的数据。MVCC 通过 Read View 机制确保只能看到已提交事务生成的版本。由于 Read View 中记录了活跃事务 ID 集合,未提交事务的修改对当前事务不可见。

6.2 解决不可重复读

RR 隔离级别下,由于整个事务复用同一个 Read View,无论其他事务如何提交,事务内多次读取的结果始终保持一致,从而解决了不可重复读。

6.3 幻读的处理

幻读是指在同一事务内执行两次范围查询,第二次查询看到了其他事务插入的新行。

MVCC 在快照读场景下可以解决幻读,因为 Read View 机制使得新插入的行对当前事务不可见。

但在当前读场景下(如 SELECT ... FOR UPDATE),需要使用 Next-Key Lock(临键锁) 来锁定范围和间隙,防止其他事务插入新行。

七、实践中的注意事项

7.1 长事务的影响

长事务会导致 Undo Log 无法及时清理,因为可能存在需要读取历史版本的老事务。这会造成:

  • Undo Log 膨胀,占用大量磁盘空间
  • 版本链过长,查询性能下降

7.2 二级索引的处理

对于二级索引,MVCC 的处理方式与聚集索引不同。二级索引记录不包含 DB_TRX_IDDB_ROLL_PTR 隐藏列。当需要判断可见性时,必须通过回表到聚集索引来获取完整的事务信息。

7.3 特殊情况:当前事务修改了其他事务更新的行

如果一个事务执行了当前读(如 UPDATE),它会读到最新版本的数据。随后该事务内的快照读也能看到自己刚刚修改的数据,即使这与 Read View 的初始快照不一致。这是 MVCC 设计中的合理例外。

八、总结

MVCC 是 MySQL InnoDB 实现高并发的核心技术,其核心思想可以概括为:

  1. 三个组件:隐藏字段构建基础,Undo Log 形成版本链,Read View 提供判断依据
  2. 两种读取:快照读依赖 MVCC 不加锁,当前读依赖锁机制保一致
  3. 一个规则:只能看到 Read View 生成时已提交的事务或自身事务的修改
  4. 两级差异:RC 级别每次查询生成新 Read View,RR 级别复用第一次生成的 Read View

通过这种精巧的设计,MVCC 使得 MySQL 能够在保证事务隔离性的同时,最大限度地提高并发性能,成为现代关系型数据库并发控制的典范。


延伸思考:虽然 MVCC 解决了读写冲突,但在高并发写入场景下,仍可能出现锁等待、死锁等问题。下一篇文章我们将深入探讨 MySQL 的锁机制,敬请期待。

暂无评论