MVCC不能彻底解决读写冲突,仅解决快照读与写之间的非阻塞并发问题;它不处理写-写冲突和当前读与写之间的冲突,后者仍依赖行锁机制。

它不能“彻底解决”读写冲突——MVCC 只解决快照读(SELECT)与写之间的阻塞问题,不解决写-写冲突,也不解决当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)与写之间的冲突。
快照读不加锁,靠的是版本链 + ReadView
普通 SELECT 在 RC 和 RR 隔离级别下默认是快照读,它不查最新行,而是沿着每行的 DB_ROLL_PTR 往上找历史版本,再用当前事务的 ReadView 判断哪个版本可见。这个过程全程不加锁,所以不会阻塞正在更新同一行的事务,也不会被对方阻塞。
关键点:
-
ReadView在 RC 下每次SELECT都新建,在 RR 下只在事务中第一次SELECT时生成——这就是 RR 能复现“不可重复读被避免”的原因 - 版本链依赖
undo log存活;如果长事务未提交,旧undo log不能清理,会导致空间膨胀和purge压力 - 只有聚簇索引记录有
DB_TRX_ID和DB_ROLL_PTR;二级索引不直接存这些字段,但查询时会回表到聚簇索引做可见性判断
当前读仍然会加锁,MVCC 不起作用
一旦语句触发当前读,比如 SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE、UPDATE、DELETE,InnoDB 就会跳过 MVCC 版本查找,直接定位最新版本行,并加行锁(或间隙锁、临键锁)。此时读写依然互斥。
常见误解场景:
- 你以为
SELECT * FROM t WHERE id = 1和UPDATE t SET x=1 WHERE id = 1并发执行不会冲突?没错,前者快照读,后者当前读+加锁,不阻塞 - 但如果你写的是
SELECT * FROM t WHERE id = 1 FOR UPDATE和UPDATE t SET x=2 WHERE id = 1—— 它们都会尝试获取同一行的 X 锁,必然发生锁等待甚至死锁 - RR 级别下幻读看似被“解决”,其实是靠
next-key lock(临键锁)+ MVCC 协同实现的,不是 MVCC 单独完成的
写-写冲突完全交给锁机制,MVCC 不参与
MVCC 对写操作只做一件事:写新版本、留旧版本、更新 DB_TRX_ID 和 DB_ROLL_PTR。但它不管两个事务同时想改同一行——那是行锁(X lock)的事。
典型表现:
- 事务 A 执行
UPDATE t SET v=10 WHERE id=1,拿到 X 锁并生成新版本 - 事务 B 紧接着执行同样语句,会被阻塞在
lock_rec_lock阶段,直到 A 提交或回滚 - 这个阻塞跟 MVCC 无关;即使关闭 MVCC(比如用 MyISAM),写-写冲突照样存在
- MVCC 的价值在于:B 在等锁时,其他不涉及 id=1 的快照读事务完全不受影响
真正容易被忽略的是:MVCC 的有效性高度依赖事务及时提交。一个没提交的长事务,会让 ReadView 中的 m_ids 列表长期包含它的 ID,导致很多旧版本无法被清理,最终拖慢整个实例的 purge 线程,甚至引发 undo log 空间耗尽。


















