在深入了解 MVCC(多版本并发控制)之前,不妨先思考一下:如果没有它,数据库的并发世界会是怎样一番景象?
在传统的并发控制机制中,数据库主要依赖锁来保证一致性。读操作加共享锁,允许多个事务同时读取,相安无事。但一旦涉及写操作,排他锁就登场了,它会阻塞其他所有的读写操作,直到锁被释放。
想象一个典型场景:你在电商平台抢购商品,查询库存(读操作)时,商家正在更新库存(写操作)。由于锁机制,你的读操作会被写操作阻塞,页面持续加载。等你好不容易刷出结果,商品早已售罄。在高并发场景下,这种读写互斥频繁发生,系统性能会严重退化,吞吐量从“高速公路”骤降为“羊肠小道”。
而 MVCC 的出现,为数据库并发控制注入了一剂“强心针”。它通过“读不阻塞写、写不阻塞读”的机制,让数据库在高并发下仍能高效运转,巧妙地避开了传统锁机制的痛点。那么,它是如何做到的呢?
一、MVCC 核心
1.1 MVCC 的官方定义
MVCC,全称 Multiversion Concurrency Control,即多版本并发控制。它是数据库领域实现事务隔离的关键机制,被广泛应用于 MySQL 的 InnoDB 引擎、PostgreSQL 等主流数据库中。
其核心思想,可以理解为给数据拍了无数张“时间快照”。每次数据修改,数据库并非直接覆盖旧数据,而是创建一个新版本,并为每个版本标记一个唯一的事务 ID,就像给照片标注拍摄时间。当事务需要读取数据时,MVCC 会根据既定规则,从历史版本中挑选出一个对当前事务可见的版本。这个过程就像读取某个过去时间点的数据快照,而非实时最新数据,从而实现了非阻塞读,在高并发场景下平衡了数据一致性与系统性能。
1.2 MVCC 的核心目标
数据库并发主要涉及三种场景:读-读、读-写和写-写。读-读操作天然无冲突,无需额外控制。但读-写和写-写场景则容易引发脏读、不可重复读、幻读等事务隔离性问题。
MVCC 的核心目标,正是为了解决读-写冲突及其引发的问题。
- 解决脏读:事务读取的是事务开始前已提交的数据版本,避免了读取到未提交的“中间状态”数据。
- 解决不可重复读(在RR级别下):在可重复读隔离级别下,事务在整个生命周期内读取的数据版本保持一致,不会因为其他事务的提交而改变。
- 局限性:对于写-写冲突(如丢失更新),MVCC 无能为力,仍需依赖锁机制来保证同一时刻只有一个事务能修改数据。因此,MVCC 和锁机制共同构成了数据库并发控制的基石。
二、MVCC 底层实现
MVCC 的实现依赖三大核心组件:隐藏字段、Undo Log 与版本链、Read View 与可见性规则。

2.1 隐藏字段:数据行的“身份标识”
在 InnoDB 存储引擎中,每行数据都包含三个隐藏字段,它们是 MVCC 的元数据基础:
DB_TRX_ID (6字节):记录最后修改(插入或更新)此数据行的事务 ID。
DB_ROLL_PTR (7字节):回滚指针,指向 Undo Log 中该数据的上一个版本。
DB_ROW_ID (6字节):隐式自增行 ID。当表未定义主键时,InnoDB 会用它来生成聚簇索引。
2.2 Undo Log 与版本链:数据的“历史档案库”
Undo Log(回滚日志)不仅用于事务回滚,更是 MVCC 存储历史版本的关键。

Undo Log 主要分为两种:
- Insert Undo Log:记录插入操作,事务提交后即可删除。
- Update Undo Log:记录更新/删除前的旧数据,是 MVCC 版本数据的核心来源。
每次数据被修改,都会生成一个新版本,旧版本通过 DB_ROLL_PTR 指针被串联起来,形成一条从新到旧的版本链。

(图:包含隐藏字段 DB_TRX_ID 和 DB_ROLL_PTR 的表结构示意)

(图:通过回滚指针 DB_ROLL_PTR 关联的版本链示意)
2.3 Read View 与可见性规则:事务的“数据透视镜”
Read View 是事务进行快照读时生成的一致性视图,决定了该事务能看到哪些数据版本。它包含四个核心参数:
m_ids:生成 Read View 时,系统中活跃(已启动未提交)事务 ID 的集合。
min_trx_id:活跃事务中最小的事务 ID。
max_trx_id:生成 Read View 时,系统将分配给下一个事务的 ID(当前最大事务ID+1)。
creator_trx_id:创建该 Read View 的事务 ID。

可见性判断规则如下(假设当前数据版本的事务 ID 为 trx_id):
- 如果
trx_id == creator_trx_id,说明当前事务正在访问自己修改过的数据,可见。
- 如果
trx_id < min_trx_id,说明该版本在生成 Read View 前已提交,可见。
- 如果
trx_id >= max_trx_id,说明该版本在生成 Read View 后才创建,不可见。
- 如果
min_trx_id <= trx_id < max_trx_id,则需要判断 trx_id 是否在 m_ids 列表中:
- 在,说明创建该版本的事务在生成 Read View 时仍活跃,不可见。
- 不在,说明该事务在生成 Read View 前已提交,可见。
如果当前版本不可见,则沿着版本链(DB_ROLL_PTR)找到上一个版本,并重复上述判断,直到找到可见的版本或遍历完版本链。
三、隔离级别下的核心差异:RC 和 RR
MVCC 在不同隔离级别下的行为不同,主要体现在 Read View 的生成时机上。
3.1 读已提交(RC)
在 RC 级别下,每次执行快照读(普通 SELECT)都会生成一个新的 Read View。
- 效果:事务能读取到其他事务最新提交的数据,解决了脏读,但会导致不可重复读。
- 示例:事务A查询余额为1000,此时事务B提交修改余额为900,事务A再次查询,会看到新的余额900。
3.2 可重复读(RR)- MySQL InnoDB 默认级别
在 RR 级别下,仅在事务第一次执行快照读时生成 Read View,后续所有快照读都复用这个视图。
- 效果:保证了事务内多次读取数据的一致性,解决了不可重复读。
- 幻读处理:InnoDB 在 RR 级别下通过间隙锁(Gap Lock)和临键锁(Next-Key Lock)来防止其他事务插入新数据,从而解决了幻读问题。而 RC 级别无法解决幻读。
四、关键概念:快照读 vs 当前读
4.1 快照读
- 定义:普通的
SELECT 语句(非加锁查询)。
- 原理:基于 MVCC 读取数据的历史快照,不加锁,实现非阻塞读。
- 可见性:由 Read View 和版本链共同决定。适用于对数据实时性要求不高的场景,如商品信息展示。
4.2 当前读
- 定义:需要获取数据最新版本的操作,如
SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT。
- 原理:读取最新已提交的数据,并对数据加锁(如排他锁)。依赖锁机制保证一致性,会阻塞其他事务的写操作。
- 场景:适用于必须强一致性的场景,如库存扣减、订单状态更新。
五、MVCC 的优缺点与优化建议
5.1 MVCC 的优势
- 高并发读:实现了读写不阻塞,极大提升了读多写少场景下的并发吞吐量。
- 保证隔离性:有效解决了脏读、不可重复读问题(RR级别下还能解决幻读)。
- 简化开发:开发者无需过度关心复杂的锁管理,降低了高并发编程的难度。
5.2 MVCC 的潜在问题
- 版本链膨胀:频繁更新会导致版本链过长,回溯历史版本时影响查询性能。
- 存储开销:Undo Log 需要存储历史版本,会占用额外磁盘空间,清理不及时可能导致空间压力。
- 维护成本:写密集型场景下,创建版本、维护版本链等操作会带来额外的 CPU 和 I/O 开销。
5.3 优化建议
- 合理选择隔离级别:根据业务对一致性和性能的需求权衡选择 RC 或 RR。
- 控制事务粒度:避免长事务,将其拆分为短小事务,及时提交,以减少版本链长度和 Undo Log 占用。
- 监控与清理:监控 Undo Log 表空间使用情况,并合理配置
innodb_purge_threads 等参数,确保历史版本及时清理。
- 规范使用:明确区分快照读和当前读的使用场景,避免在不需要最新数据的地方使用当前读导致不必要的锁竞争。
六、总结
MVCC 通过“以空间换时间”的策略,维护数据的多个版本,巧妙地解决了数据库读写并发冲突。其三大支柱——隐藏字段、Undo Log 版本链、Read View——协同工作,精准控制事务的数据可见性。
理解快照读与当前读的区别,以及 MVCC 在 RC 和 RR 隔离级别下的不同行为,对于设计高性能、高并发的数据库应用至关重要。虽然 MVCC 会带来额外的存储和维护开销,但在大多数读多写少的现代应用场景中,它带来的并发性能提升是无可替代的。
希望本文能帮助你深入理解 MVCC 这一数据库核心并发控制机制。如果你想进一步探讨数据库或其他后端技术,欢迎访问 云栈社区 与更多开发者交流。