MVCC 是 MySQL(尤其是 InnoDB 存储引擎)实现高并发、高性能的核心机制之一。理解了 MVCC,就理解了 MySQL 事务隔离级别的底层实现。


什么是 MVCC?

MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制

  • 核心思想: 为了应对高并发,不再让读写操作相互阻塞,而是通过一种“快照”的方式,为每个事务在开始时提供一个一致性的数据视图。
  • 实现方式: 在每一行记录的背后,InnoDB 会保存多个版本的数据(通过 Undo Log 实现)。当不同的事务同时读写同一行时,它们看到的是这行数据在不同时间点的“快照”,而不是直接操作同一份数据本身。这样就实现了非阻塞的读操作,写操作也只需要锁定必要的行,极大提高了并发性能。

MVCC 的底层支撑:关键概念

要理解 MVCC,必须先了解以下几个核心概念:

a. 隐藏字段

InnoDB 为每一行数据(记录)都添加了三个用户看不见的隐藏字段:

  1. DB_TRX_ID (6字节)事务ID。表示最后一次插入或更新该行记录的事务ID。删除在 InnoDB 内部也被视为一次更新,会设置一个特殊的标记位。
  2. DB_ROLL_PTR (7字节)回滚指针。指向该行记录上一个版本Undo Log 中的地址。所有旧版本的数据都通过这个指针连接成一个版本链(类似一个链表)。
  3. DB_ROW_ID (6字节)行ID。如果表没有定义主键,InnoDB 会自动生成一个隐藏的主键 using this field。它与 MVCC 关系不大,主要是用于构建聚簇索引。

b. Undo Log (回滚日志)

  • 作用: 用于存储数据被修改之前的旧值(旧版本)。
  • 与 MVCC 的关系: 当执行 UPDATEDELETE 操作时,旧版本的数据并不会被立即物理删除,而是会被拷贝到 Undo Log 中,形成一个版本链。这个版本链就是 MVCC 实现多版本数据的核心
  • 生命周期: 当事务提交后,对应的 Undo Log 不会立刻删除,因为可能还有其他正在运行的事务需要读取这个旧版本的数据。只有当系统中没有比这个 Undo Log 更早的读视图(ReadView)时,它才会被 Purge 线程清理掉。

c. Read View (读视图)

  • 是什么: Read View 是事务在进行快照读(普通的 SELECT 语句)时产生的一个一致性视图。它决定了这个事务能看到哪个版本的数据,看不到哪个版本的数据。
  • 关键组成部分: 一个 Read View 主要包含以下几个关键属性:
    • m_ids: 生成 Read View 时,系统中所有活跃(尚未提交)的事务ID的集合。
    • min_trx_idm_ids 中的最小值。
    • max_trx_id: 生成 Read View 时,系统应该分配给下一个事务的ID(即当前最大事务ID + 1)。
    • creator_trx_id创建这个 Read View 的事务的ID(只有执行写操作的事务才有ID,只读事务的ID为0)。

MVCC 的工作流程:版本链与可见性算法

现在我们结合以上概念,来看一个 SELECT 语句是如何通过 MVCC 找到它应该看到的那个数据版本的。

假设我们有一行数据,经过多个事务修改,形成了一个版本链(通过 DB_ROLL_PTR 指针连接)。

版本 事务ID (DB_TRX_ID) 数据内容 回滚指针 (DB_ROLL_PTR)
V4 105 ... 指向 V3 在 Undo Log 中的位置
V3 102 ... 指向 V2 在 Undo Log 中的位置
V2 101 ... 指向 V1 在 Undo Log 中的位置
V1 100 ... NULL

现在,一个 事务ID=103 的事务发起了一个快照读(SELECT),它生成了一个 Read View,假设此时:

  • m_ids = [101, 102] (事务101和102都还未提交)
  • min_trx_id = 101
  • max_trx_id = 104
  • creator_trx_id = 103

这个事务会从最新的版本 V4 开始,沿着版本链依次判断每个版本是否对自己可见。可见性判断规则如下:

  1. 如果被访问版本的 DB_TRX_ID 等于 creator_trx_id,说明这是当前事务自己修改的,可见
  2. 如果被访问版本的 DB_TRX_ID 小于 min_trx_id,说明这个版本是在当前 Read View 创建之前就已经提交的,可见
  3. 如果被访问版本的 DB_TRX_ID 大于等于 max_trx_id,说明这个版本是在当前 Read View 创建之后才开启的事务修改的,不可见
  4. 如果被访问版本的 DB_TRX_IDmin_trx_idmax_trx_id 之间(即 m_ids 的范围内),则需要判断 DB_TRX_ID 是否在 m_ids 列表中:
    • 如果在,说明创建 Read View 时,修改这个版本的事务还处于活跃状态(未提交),该版本不可见
    • 如果不在,说明创建 Read View 时,修改这个版本的事务已经被提交了,该版本可见

如果某个版本对当前事务不可见,就顺着回滚指针 DB_ROLL_PTR 找到上一个版本,重复上述判断规则,直到找到第一个可见的版本为止。

套用到我们的例子:

  • 判断 V4 (trx_id=105):
    • 105 >= max_trx_id=104,满足规则3,不可见。找上一个版本 V3。
  • 判断 V3 (trx_id=102):
    • 102 在 min_trx_id=101max_trx_id=104 之间。
    • 检查 m_ids=[101,102],102 在列表中,满足规则4,不可见。找上一个版本 V2。
  • 判断 V2 (trx_id=101):
    • 101 在 min_trx_id=101max_trx_id=104 之间。
    • 检查 m_ids=[101,102],101 在列表中,满足规则4,不可见。找上一个版本 V1。
  • 判断 V1 (trx_id=100):
    • 100 < min_trx_id=101,满足规则2,可见

最终,事务103 读到的就是 V1 这个旧版本的数据。


MVCC 与不同事务隔离级别的交互

MVCC 的行为会根据事务的隔离级别发生变化,主要体现在 Read View 生成的时机不同。

隔离级别 生成 Read View 的时机 MVCC 行为说明
读未提交 (READ UNCOMMITTED) N/A 根本不使用 MVCC。直接读取数据的最新版本,无论是否提交,所以会脏读
读已提交 (READ COMMITTED) 每次执行快照读(SELECT)时 每次 SELECT 都会生成一个新的 Read View。这保证了每次读都能看到最新已经提交的数据,解决了脏读,但可能导致不可重复读幻读
可重复读 (REPEATABLE READ) 第一次执行快照读(SELECT)时 只在第一次 SELECT 时生成一个 Read View,后续所有的 SELECT 操作都复用这个 Read View。这保证了在整个事务中,看到的数据内容是一致的(第一次读时的快照),解决了不可重复读。通过 Next-Key Lock 机制在一定程度上缓解了幻读。
串行化 (SERIALIZABLE) N/A 不使用 MVCC。所有读操作都会对记录加共享锁(S锁),读写相互阻塞,通过强制事务串行来解决问题。

举例说明 RR 和 RC 的区别:

假设账户余额为 100,有两个事务:

  • 事务A(ID=101):启动后查询余额,得到 100。

  • 事务B(ID=102):更新余额为 200 并提交

  • 事务A:再次查询余额。

  • RC 级别下:

    • 事务A的第二次查询会重新生成一个 Read View。此时系统中已无活跃事务,事务B(102)已提交。根据规则,它能看见 102 修改的版本,所以查询结果为 200。(不可重复读)
  • RR 级别下:

    • 事务A的第二次查询会复用第一次生成的 Read View。在那个旧的 Read View 中,事务B(102)可能还是活跃的(或在其之后),所以根据可见性规则,它看不到 102 提交的版本,只能看到更早的版本,查询结果依然是 100。(可重复读)

总结:MVCC 的优缺点

优点:

  1. 高并发: 读不加锁,读写操作不互相阻塞,大大提高了系统的并发处理能力。
  2. 保证一致性: 提供了基于时间点的一致性视图,保证事务的隔离性。

缺点:

  1. 额外存储: 需要额外的空间(Undo Log)来存储旧版本数据。
  2. 维护开销: 需要维护版本链,并且 Purge 线程需要定期清理不再需要的旧版本数据。
  3. 部分问题: 在 RR 级别下,MVCC 并不能完全解决幻读问题(需要通过 Next-Key Lock 来解决)。