快照读复用readview走mvcc版本链,当前读绕过mvcc直接读最新数据并加锁;二者路径不同导致结果不一致,非锁机制问题。

快照读复用ReadView,当前读穿透MVCC读最新版本
RR下快照读和当前读结果不一致,不是因为“锁没加对”,而是它们根本走的是两条路径:快照读靠MVCC固定视图,当前读绕过MVCC直接查最新数据并加锁。事务启动时不生成ReadView,第一次SELECT才创建;之后所有快照读都复用它。而SELECT ... FOR UPDATE、UPDATE、DELETE这类语句一执行,就立刻丢掉ReadView,去聚簇索引找最新已提交版本。
常见错误现象包括:
- 事务A先
SELECT * FROM t WHERE b = 1返回1行 → 事务B插入b = 1新行并提交 → 事务A再执行UPDATE t SET c = 100 WHERE b = 1,成功更新2行 - 同一事务内两次
SELECT结果一样,但SELECT ... FOR UPDATE突然多出一行
Next-Key Lock只在当前读生效,且依赖索引条件
很多人以为RR“默认防幻读”,其实Next-Key Lock(记录锁+间隙锁)只在当前读触发,且有硬性前提:WHERE条件必须命中有效索引、最好是范围查询。快照读完全不触发任何锁,既不阻止插入,也不感知新行。
典型失效场景:
-
WHERE name = 'Alice'但name列无索引 → 全表扫描,间隙锁退化为行锁甚至不生效 -
WHERE id = 100(主键等值)→ 只加记录锁,不锁间隙,别人仍可在99和101之间插入 - 非唯一二级索引上等值查询(如
INDEX(b),b = 10有多行)→ 锁区间可能漏锁或过度锁,边界判断复杂
长事务让快照读变慢,不是因为锁,而是undo log无法purge
快照读本身不加锁,但它依赖的undo log版本链不能被清理。如果一个事务迟迟不提交,它的ReadView会一直拦着purge线程,导致undo表空间持续膨胀、历史版本链拉长——所有快照读都要遍历更长的版本链,最终拖慢整个实例。
这不是锁等待,是MVCC机制的副作用。表现常为:
- 普通
SELECT响应时间逐渐变长,尤其带WHERE条件时 -
INFORMATION_SCHEMA.INNODB_TRX里看到trx_started时间极早的长事务 -
SHOW ENGINE INNODB STATUS中HISTORY LIST长度持续增长
混用快照读做业务判断,等于把逻辑建在过期视图上
最危险的不是“读不到新数据”,而是误把快照读结果当事实依据。比如用SELECT COUNT(*) FROM orders WHERE user_id = 123得0,就认为可以插入;结果别的事务刚插入并提交,你的INSERT(当前读)就看到那条记录,可能触发唯一键冲突或业务逻辑错乱。
这种问题不会报错,但会导致状态不一致。关键点在于:
- 快照读回答的是“当时有哪些”,不是“现在能操作哪些”
- 当前读才是真实操作面,它看到的数据集永远比快照读“多”或“不同”
- 只要事务里出现一次当前读,后续快照读和当前读就不再具有可比性
真正难处理的,从来不是锁怎么加,而是业务代码里那些没意识到自己在混用两种读语义的if分支和count校验。











