一 MVCC 的核心目标:读写不阻塞
在传统的锁机制中,写操作会阻塞读操作。而 MVCC 让 InnoDB 实现了:
- 非锁定读(Consistent Read):普通的
SELECT不加锁。 - 并发读写:写操作时,读操作可以读取数据的“历史版本”,互不干扰。
MVCC,即多版本并发控制。在MySQL InnoDB中,它的核心目的是解决读写冲突,实现非锁定一致性读。简单来说,它让
SELECT操作在读取数据时,不需要等待UPDATE或DELETE释放锁,而是通过读取数据行的历史快照来返回结果,从而极大提升数据库的并发性能。
二 核心三要素
1. 隐藏字段
每行数据除了我们定义的列,还有三个隐藏字段:
- DB_TRX_ID:最后修改(插入/更新/删除)该行的事务ID。
- DB_ROLL_PTR:回滚指针,指向该行数据在undo log中的历史版本。
- DB_ROW_ID:隐含的自增ID(如果表没有主键时才会用到,面试时可简略带过)。
2. undo log(版本链)
- 当一行数据被修改时,InnoDB会先将旧数据写入undo log。
- 通过
DB_ROLL_PTR,最新数据指向旧数据,形成一条版本链。 - 这条链就是MVCC能够“回溯”到历史版本的依据。
3. Read View(读视图)
这是核心控制逻辑。当执行SELECT(快照读)时,会生成一个Read View,它包含几个关键属性:
- m_ids:生成Read View时,当前系统正在执行、还没提交的事务 ID 列表。
- min_trx_id:m_ids中的最小值。
- max_trx_id:生成Read View时系统应该分配给下一个事务的ID(即m_ids中最大值+1)。
- creator_trx_id:当前执行
SELECT操作的事务自己的ID。
三 给定一个版本链,怎么判断哪个版本对当前事务可见?
规则如下: 当事务尝试读取某行记录时,会拿着该记录的 DB_TRX_ID 与 Read View 进行对比,逻辑如下:
- 如果
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列表中。- 如果在(即还未提交):不可见,顺着
DB_ROLL_PTR找上一个版本。 - 如果不在(即已经提交了):可见。
- 如果在(即还未提交):不可见,顺着
如果当前版本不可见,就顺着回滚指针去 Undo Log 找上一个版本,重复上述判断。
四 RC 与 RR 隔离级别的区别
MVCC 在不同的隔离级别下,生成 Read View 的时机不同,这直接导致了现象的差异:
- READ COMMITTED (RC):每次
SELECT都会生成一个新的 Read View。所以能看到其他事务已提交的修改(导致不可重复读)。 - REPEATABLE READ (RR):只在第一次
SELECT时生成 Read View,后续所有查询复用同一个。所以能保证在整个事务期间看到的数据是一致的。
五 延展
快照读和当前读的区别?
- 快照读:普通的
SELECT语句(不加锁),读取的是Read View下的可见版本,不等待锁。 - 当前读:
SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT等操作,读取的是数据的最新版本,并且会对记录加锁(行锁/间隙锁)。
一句话总结:MVCC让快照读无锁,当前读负责数据一致性。
RR级别下能完全避免幻读吗?
- MVCC通过Read View在RR级别下避免了快照读的幻读(即同一事务内多次
SELECT结果一致)。 - 但如果是当前读(比如
SELECT ... FOR UPDATE),MySQL会结合**Next-Key Lock(间隙锁+行锁)**来防止其他事务插入新数据,从而彻底解决幻读。
长事务会有什么问题?
- 因为MVCC依赖undo log构建历史版本,如果存在一个长事务,它的Read View会一直保留,导致undo log不能被清理。
- 后果:undo log膨胀,占用大量存储空间,甚至导致性能下降(“undo log爆炸”)。
六 总结: MVCC 解决了什么?
“总的来说,MVCC通过隐藏字段 + undo log版本链 + Read View可见性判断,实现了读写并发的高效处理。它是InnoDB区别于MyISAM、实现高并发OLTP场景的核心能力之一。在实际开发中,理解MVCC能帮助我们更好地设计事务粒度,避免长事务,并准确判断不同隔离级别下的数据可见性。”
- 解决了脏读:通过版本链和 Read View 过滤未提交数据。
- 解决了不可重复读:RR 级别下复用 Read View。
- 很大程度上缓解了幻读:通过快照读避开了其他事务插入的新记录(虽然彻底解决幻读还需配合 Next-Key Locks)。
注意:MVCC 只在 RC 和 RR 两个隔离级别下工作。Serializable(串行化)通过加锁实现,Read Uncommitted(读未提交)直接读最新数据,都不需要 MVCC。