在数据库系统中,并发控制是一个经典且复杂的话题。当多个事务同时对同一份数据进行读写操作时,如何保证数据的一致性、隔离性,同时又能兼顾高性能?MySQL InnoDB 引擎给出的答案之一就是 MVCC(Multi-Version Concurrency Control,多版本并发控制)。
本文将从为什么需要 MVCC 、MVCC 核心原理 、隐藏字段与版本链 、一致性视图(Read View) 以及可见性判断规则五个方面,由浅入深地为你彻底讲透 MVCC。
一、为什么需要 MVCC?
想象一个场景:
-
事务 A 正在读取用户表中的数据(耗时较长的统计查询)。
-
与此同时,事务 B 正在修改同一条数据(比如更新余额)。
如果使用传统的加锁并发控制(如两阶段锁),那么事务 A 在读取时,事务 B 必须等待,反之亦然。这会导致数据库的并发能力严重下降,响应变慢。
MVCC 的思想是:读不阻塞写,写不阻塞读。
它通过保存数据在某个时间点的快照,让读操作可以看到该时间点之前已提交的数据版本,而不受后续写操作的影响。这样一来,读写事务之间无需互斥等待,极大地提升了并发性能。
二、MVCC 的核心概念
1. 什么是 MVCC?
MVCC(Multi-Version Concurrency Control)是一种并发控制方法,它允许数据库在读写并发时,为每个读操作提供一个一致性快照,从而实现非锁定读取。
MVCC 仅在 Read Committed(已提交读) 和 Repeatable Read(可重复读) 两种隔离级别下生效,因为这两种级别需要"读到的数据是一致的"。而在 Read Uncommitted 下,不需要一致性视图;在 Serializable 下,则通过加锁实现完全隔离。
2. 核心思想:保留多个版本
传统更新是"覆盖旧数据",而 MVCC 的更新是**"保留旧版本,新增新版本"**:
-
当更新一条记录时,InnoDB 不会直接覆盖原数据。
-
而是将旧数据保留在 undo log 中,并生成一条新版本的数据。
-
不同事务根据自身的事务 ID 和创建时间,决定能看到哪个版本。
三、InnoDB 中的隐藏字段与版本链
为了实现 MVCC,InnoDB 在每行记录中悄悄添加了三个隐藏字段(你平时用 SELECT * 是看不到的):
| 隐藏字段 | 含义 |
|---|---|
| DB_TRX_ID | 最近一次插入或更新该行的事务 ID(事务版本号) |
| DB_ROLL_PTR | 回滚指针,指向该行旧版本的 undo log 记录 |
| DB_ROW_ID | 行 ID(如果表没有主键,InnoDB 会用该字段生成聚簇索引) |
版本链(Version Chain)
每次更新操作,InnoDB 会:
-
将当前数据行的旧值复制到 undo log 中。
-
更新原数据行的
DB_TRX_ID为当前事务 ID。 -
将
DB_ROLL_PTR指向刚写入的 undo log 记录。
这样一来,同一行数据的不同版本通过 DB_ROLL_PTR 串联成一条版本链,从最新版本一直回溯到最旧的版本。
最新版本 (DB_TRX_ID = 事务5)
↑
│ (DB_ROLL_PTR)
↑
中间版本 (DB_TRX_ID = 事务3)
↑
│ (DB_ROLL_PTR)
↑
最旧版本 (DB_TRX_ID = 事务1)
四、一致性视图(Read View)------ 快照的核心
当执行一个查询语句(SELECT)时,InnoDB 会为该查询生成一个 Read View(一致性视图)。这个视图决定了当前查询能看到哪些版本的数据。
Read View 的核心组成包括:
-
m_ids :当前系统中所有未提交的事务 ID 数组(不包括当前事务自己)。
-
min_trx_id:m_ids 中的最小值(即最早未提交事务的 ID)。
-
max_trx_id :系统下一个将要分配的事务 ID(即当前已创建的最大事务 ID + 1)。
注意:
max_trx_id并不是当前已提交的最大事务 ID,而是"下一个即将使用的 ID",因此它一定大于所有已存在的事务 ID。
五、可见性判断规则(核心中的核心)
有了 Read View 之后,查询会沿着版本链从最新版本开始,逐个往前比对,找到第一个可见的版本。
比对规则如下(我们把当前要判断的版本的事务 ID 记为 trx_id):
| 条件 | 结论 | 解释 |
|---|---|---|
| trx_id < min_trx_id | ✅ 可见 | 该版本是在所有未提交事务之前就已经提交的,是稳定的历史数据。 |
| trx_id >= max_trx_id | ❌ 不可见 | 该版本是由未来才启动的事务产生的(当前查询发生时它还不存在),肯定不可见。 |
| min_trx_id <= trx_id < max_trx_id | 需要进一步判断 | 该版本可能来自已提交或未提交的事务,需要查 m_ids。 |
| ↳ trx_id 在 m_ids 中 | ❌ 不可见(特殊情况下当前事务自己的更新可见) | 该版本来自一个尚未提交的事务,对其他事务来说不应看到。 |
| ↳ trx_id 不在 m_ids 中 | ✅ 可见 | 该版本来自一个已经提交的事务,可以安全读取。 |
补充说明(非常重要):
-
当前事务自己修改过的数据 ,即使
trx_id在 m_ids 中(因为自己还未提交),但对自己的查询是可见的。这是出于事务内部一致性的需要。 -
对于 删除操作 ,InnoDB 将其视为一种特殊的更新:它会复制最新版本的数据,将
DB_TRX_ID改为删除事务的 ID,并在记录头信息中将deleted_flag标记为true。查询时如果命中了被标记删除的记录,则视为不可见,不返回该行。
六、图解 MVCC 可见性判断
为了方便理解,我们用一张图来总结(颜色代表不同类型的事务):

七、RR 与 RC 下 MVCC 的区别
| 隔离级别 | Read View 生成时机 | 效果 |
|---|---|---|
| Read Committed | 每次执行 SELECT 语句时都重新生成一个新的 Read View |
能看到其他事务已提交的最新更改,但同一事务内多次读取可能不一致(不可重复读)。 |
| Repeatable Read | 事务中第一次 执行 SELECT 时生成 Read View,之后复用 |
整个事务期间看到的数据快照一致,保证可重复读。 |
这也解释了为什么 RR 级别能避免不可重复读和幻读(结合间隙锁),而 RC 级别不能。
八、MVCC 的优势与局限
优势
-
✅ 读操作无锁 :普通
SELECT不需要加任何锁,读写互不阻塞,高并发下性能极佳。 -
✅ 一致性保证:提供基于时间点的数据快照,满足事务隔离性要求。
-
✅ 回滚支持:undo log 中的版本链本身也是事务回滚的数据基础。
局限
-
❌ 存储开销:每个更新都会产生 undo log 版本,如果事务长期不提交或并发极高,undo log 会膨胀,占用大量存储空间。
-
❌ 清理负担:后台的 purge 线程需要定期清理那些所有事务都不再需要的旧版本数据,会增加系统负载。
九、总结
MVCC 是 MySQL InnoDB 引擎实现高并发、高一致性读的基石。它通过:
-
隐藏字段(DB_TRX_ID + DB_ROLL_PTR) 记录版本信息;
-
undo log 版本链 保存历史数据;
-
一致性视图(Read View) 在查询时动态判断可见性;
-
可见性算法 精确控制每个事务能看到的数据版本。