MySQL靠MVCC避免读写冲突,即通过DB_TRX_ID和DB_ROLL_PTR隐藏字段维护多版本快照,结合read view实现无锁读;但UPDATE/DELETE仍需加行锁,且幻读在当前读下需间隙锁补充。

MySQL怎么靠MVCC避免读写冲突
MVCC(多版本并发控制)不是锁,而是给每一行数据悄悄存多个“快照版本”,让不同事务看到各自需要的那一个。核心是靠DB_TRX_ID(最近修改该行的事务ID)和DB_ROLL_PTR(指向undo log里旧版本的指针)两个隐藏字段配合实现。
实际读取时,InnoDB不直接查最新数据,而是根据当前事务的read view(快照视图)决定:哪些版本可见、哪些被忽略。比如一个事务启动时拿到的read view里记录了当时活跃事务ID列表,它就会跳过所有未提交或比自己晚开始的修改。
- 可重复读(RR)隔离级别下,事务第一次执行SELECT时才生成
read view,后续查询复用——所以能保证“可重复读” - 读已提交(RC)则每次SELECT都新建
read view,因此能看到其他事务刚提交的新数据 - 注意:MVCC只对
SELECT生效;UPDATE/DELETE仍要加行锁,防止覆盖写
为什么UPDATE会阻塞但SELECT不会
因为MVCC让SELECT走的是无锁路径,而UPDATE必须定位到最新版本行并加X锁,同时还要检查是否满足更新条件(比如WHERE子句),这就绕不开当前值和锁状态。
常见误解是“MVCC让所有操作都不锁”,其实它只解耦了读和写,没解除写和写的竞争。比如两个事务同时想改同一行,后到的那个会被LOCK_WAIT卡住,直到前一个提交或回滚。
- 查看锁等待:
SELECT * FROM performance_schema.data_locks或SHOW ENGINE INNODB STATUS里的TRANSACTIONS部分 -
SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE是显式触发加锁的SELECT,不再走纯MVCC路径 - 如果WHERE条件没走索引,InnoDB可能升级为表级锁(尤其是5.7之前),导致误伤无关行
undo log版本链太长会拖慢查询吗
会。虽然MVCC本身不加锁,但如果某一行被频繁修改,它的undo log版本链就很长,每次查找可见版本就得顺着DB_ROLL_PTR一路往前遍历,直到找到第一个符合条件的版本。极端情况下,这个链可能上百层,明显拉高SELECT延迟。
这不是理论风险——尤其在长时间运行的事务+高频更新场景下(比如监控类任务忘了COMMIT),很容易积累大量不可见旧版本。
- 监控方法:
SHOW ENGINE INNODB STATUS中关注HISTORY LIST长度,超过几万就要警惕 - 根本解法是缩短事务生命周期,避免在事务里做HTTP调用、文件读写等耗时操作
-
innodb_max_purge_lag可以间接压制purge速度,但治标不治本;重点还是业务逻辑里尽早提交
RR隔离级别下幻读真的完全消失了吗
没有。MVCC解决了快照读(普通SELECT)的幻读,但解决不了当前读(SELECT ... FOR UPDATE、UPDATE、INSERT)的幻读。InnoDB用间隙锁(Gap Lock)和临键锁(Next-Key Lock)来堵住这个缺口。
比如SELECT * FROM t WHERE id > 10 FOR UPDATE,不仅锁住现有id>10的记录,还会锁住这些记录之间的“间隙”,防止新记录插入进来破坏一致性。
- 间隙锁在唯一索引且等值查询时失效(如
WHERE id = 15),此时只锁记录本身 - READ COMMITTED隔离级别下,InnoDB默认关闭间隙锁,所以RC天然允许幻读(这也是RC比RR并发性略高的代价)
- 如果你关了
innodb_locks_unsafe_for_binlog(5.6+已废弃),或者用了唯一索引+等值条件,间隙锁可能意外失效
真正容易被忽略的点在于:你以为自己在用MVCC躲开了所有锁,结果一条FOR UPDATE就把整个范围锁死了,还查不出原因——因为锁信息不在SQL里,而在索引结构和隔离级别共同作用下悄悄生成。


















