MVCC(Multi-Version Concurrency Control,多版本并发控制)是MySQL InnoDB引擎实现高性能并发的关键机制。简单来说,它让数据库在读写并发的场景下,读操作不再需要等待写操作,写操作也不会阻塞读操作,显著提升了系统的吞吐量。
它通过在每行记录上保存多个版本,并为不同事务创建一致性快照(Read View),让事务看到自己“应该看到”的数据版本(如同一事务内始终读同一个快照,实现“可重复读”)。
⚙️ MVCC的三大核心组件
MVCC的实现依赖于以下三个在InnoDB表隐式存在的结构:
| 三个隐藏字段 | InnoDB为每行记录自动添加了三个隐藏字段。 |
| Undo Log 版本链 | 记录了数据行的所有历史版本,通过回滚指针串联形成一个单向链表。 |
| Read View 读视图 | 事务执行快照读时生成的一致性快照,用于判断版本链中哪个版本对当前事务可见。 |
这三个组件共同协作,构成了MVCC的基石。
1. 三个隐藏字段
InnoDB为每行记录自动添加了以下隐藏列:
- DB_TRX_ID (6字节):记录最后修改该行的事务ID,是进行可见性判断的核心依据。
- DB_ROLL_PTR (7字节):回滚指针,指向该行在Undo Log中的上一个版本,用于构建版本链。
- DB_ROW_ID (6字节):单调递增的行ID。当表没有定义主键时,InnoDB会用它生成隐藏的聚簇索引。
2. Undo Log 版本链
当一行数据被修改时,InnoDB不会直接覆盖旧值,而是执行以下操作:
- 将被修改行的旧数据写入Undo Log。
- 将该行当前的 DB_TRX_ID 更新为当前修改事务的ID。
- 将当前行的 DB_ROLL_PTR 更新为指向刚写入的Undo Log的指针。
- 通过这种方式,每一次修改都会在Undo Log中形成一个“旧版本”,并被 DB_ROLL_PTR 串联起来,形成一个按时间顺序从新到旧排列的版本链。
📌 示例场景
假设每个事务修改后都立即提交(为了简化,实际未提交时也会产生版本链)。
🧬 详细版本链结构图
【当前版本】聚簇索引中的记录(事务102修改后)
┌───────────────────────────────────────────────┐
│ 实际数据: id=1, name='C', age=22 │
│ DB_TRX_ID = 102 │
│ DB_ROLL_PTR ──────────────────────────────┐ │
└───────────────────────────────────────────┼────┘
│
▼
┌────────────────────────────────────────────────┐
│ Undo Log 记录(版本 V2) │
│ 变更前数据: id=1, name='B', age=21 │
│ 该版本的 DB_TRX_ID = 101 │
│ 该版本的 DB_ROLL_PTR ──────────────────────┐ │
│ 额外元数据:表空间ID、页号、记录偏移 等 │
└───────────────────────────────────────────┼─────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Undo Log 记录(版本 V1) │
│ 变更前数据: id=1, name='A', age=20 │
│ 该版本的 DB_TRX_ID = 100 │
│ 该版本的 DB_ROLL_PTR = NULL (无更旧版本) │
│ 额外元数据:表空间ID、页号、记录偏移 等 │
└──────────────────────────────────────────────────┘
3. Read View 读视图
当事务执行快照读(普通的SELECT查询)时,InnoDB会为这次查询生成一个Read View(快照)。这个视图记录了在该时刻数据库的系统状态,主要包含四个核心属性:
| creator_trx_id | 创建当前Read View的事务ID。 |
| m_ids | 在生成Read View时,系统中所有活跃且未提交的事务ID列表。 |
| min_trx_id | m_ids列表中的最小值,即当前活跃事务中最老的事务ID。 |
| max_trx_id | 系统即将分配给下一个事务的事务ID(max_trx_id = 当前最大事务ID + 1)。 |
注意:不同隔离级别下,Read View的生成策略不同。在READ COMMITTED级别,每次查询都会生成新的Read View;在REPEATABLE READ级别,整个事务只使用第一个查询生成的Read View。
🎯 可见性判断算法 (Read View如何工作)
当事务发起快照读时,InnoDB会沿着版本链,从最新的版本开始,为每个版本的DB_TRX_ID执行以下判断,直到找到第一个符合条件的版本:
通过这套严谨的算法,每个事务都能获得一个与自己隔离级别相符、一致的数据快照。
🔍 快照读 vs. 当前读
MVCC并非适用于所有SQL语句,它主要作用于快照读。理解快照读与当前读的区别至关重要:
| 快照读 | 读取数据的历史快照版本,不加锁,非阻塞。 | 普通的SELECT语句。 | 不加锁 |
| 当前读 | 读取数据的最新提交版本,并对其加锁,防止并发修改。 | SELECT … FOR UPDATESELECT … LOCK IN SHARE MODEUPDATE …DELETE …INSERT … | 行锁、间隙锁、Next-Key Lock等 |
实践提醒:当你需要基于可重复读的快照进行更新操作时,需要特别小心。例如,在一个事务中先用快照读查询了数据,然后再执行更新操作(当前读),可能会出现"更新丢失"的问题。建议在RR级别下,如果后续要更新数据,应使用SELECT … FOR UPDATE进行当前读,确保读到最新值。
🔬 RC vs. RR:MVCC如何实现不同隔离级别
MVCC是**READ COMMITTED(读已提交,RC)和REPEATABLE READ(可重复读,RR)隔离级别的底层实现基础。二者实现的核心区别在于Read View的生成策略**:
| READ COMMITTED (RC) | 语句级快照:事务中每条SELECT语句执行时,都会生成一个全新的Read View。 | 每次查询都获取最新的快照,因此可能读到其他事务已提交的新数据,导致同一事务内多次读取结果可能不一致,即“不可重复读”。 |
| REPEATABLE READ (RR) | 事务级快照:事务中只在第一次执行SELECT(快照读)时生成一个Read View,后续的所有SELECT都复用它。 | 整个事务的查询都基于最初的那个快照,因此多次读取同一数据的结果始终一致,避免了“不可重复读”问题。 |
注意:关于“幻读”,MVCC的 Read View 机制只解决了快照读场景下的幻读问题。但当发生当前读时,MySQL InnoDB 引擎还需要借助 Next-Key Lock(记录锁+间隙锁) 来阻止其他事务插入新记录,以避免幻读的发生。
※RR 隔离级别下,没有当前读时,如何解决幻读?
首先明确“幻读”的定义
幻读是指:在同一事务内,两次相同的范围查询(例如 SELECT * FROM t WHERE id > 10)返回的记录条数不一致,因为另一个事务插入了符合条件的新记录。
注意:幻读强调的是 插入 操作导致的“多出几行”,与不可重复读(针对已存在行的更新)不同。
RR 隔离级别下,快照读(普通 SELECT)避免幻读的原理
在 RR 级别,事务内第一次执行快照读时,会生成一个 Read View(即一致性视图),之后该事务的所有快照读都复用这个 Read View,不再重新生成。
这个 Read View 记录了“当时”数据库中所有活跃事务的 ID 列表,以及已提交事务的最大 ID 等信息。
当后续查询再次扫描同一个范围时,依然使用同一个 Read View,因此:
- 即使其他事务在两次查询之间插入了新行并提交了,这些新行的 DB_TRX_ID 一定大于等于生成 Read View 时的 max_trx_id(因为它们是后来才提交的事务)。
- 根据 MVCC 可见性算法,DB_TRX_ID >= max_trx_id 的行对当前 Read View 是不可见的(属于“未来的事务”)。
- 所以,第二次快照读的结果集与第一次完全一样,看不见新插入的行,幻读因此被避免。
举例说明
— 事务 A(RR 隔离级别)
START TRANSACTION;
— 第一次快照读,生成 Read View
SELECT * FROM user WHERE age > 18; — 返回 2 行
— 事务 B(并发执行)
START TRANSACTION;
INSERT INTO user (age) VALUES (25); — 插入新行
COMMIT;
— 事务 A 再次快照读(复用之前的 Read View)
SELECT * FROM user WHERE age > 18; — 依然返回原来的 2 行,没有幻读
COMMIT;
重要补充说明
- 以上机制只对快照读有效。如果事务 A 在第二次查询之前执行了 当前读(如 SELECT … FOR UPDATE 或 UPDATE …),那么当前读会看到其他事务已提交的最新数据(因为它读取的是最新版,并加锁),此时可能产生幻读。
- 若要彻底消除幻读(包括当前读场景),InnoDB 需要配合 间隙锁(Gap Lock) 或 Next-Key Lock,但这超出了“没有当前读”的范畴。
结论
- 在 RR 隔离级别下,只要事务中的所有查询都是快照读(普通 SELECT),没有穿插任何当前读,那么 MVCC 就能完美解决幻读问题。
- 这是由于 Read View 的事务级复用,使得事务始终看到与第一次查询时一致的数据快照,新插入的行被自动过滤。
⚖️ MVCC的优缺点
优点:
- 读写不阻塞:提升高并发读写场景下的吞吐量。
- 解决常见隔离性问题:在RC和RR级别下,避免脏读,并在RR级别下避免不可重复读和快照读幻读。
- 高并发读性能:显著提升读操作性能。
缺点:
- 存储空间开销:Undo Log占用额外空间,频繁更新会导致其膨胀。
- 写冲突风险:不解决“写-写”冲突,仍需依赖锁机制。
- 历史版本维护成本:如果存在长事务,会阻止Undo Log清理,导致版本链过长,影响查询性能。
- 查询性能开销:需要遍历版本链进行可见性判断,增加查询成本。
💎 总结
MVCC 让 InnoDB 在大部分情况下,可以无锁地实现高效、一致的快照读,同时区分了 RC 和 RR 两种隔离级别。
在实际的开发中,掌握 MVCC 可以帮助做更合理的数据库设计:
- 选择合适的隔离级别:在大多数读多写少的业务场景下,充分利用 RR 级别默认带来的强大一致性保证;在对一致性要求不那么严格但对并发敏感的场景,可酌情考虑切换至 RC 级别以降低锁开销。
- 规避长事务:长事务会阻止 Undo Log 的回收,导致磁盘空间膨胀和历史版本链过长,严重影响数据库性能。务必确保生产环境中的事务代码在完成操作后立即提交。
- 理解快照读与当前读的差异:如上所述,这是正确编写并发程序的关键。


