mysql的rr隔离级别分场景解决幻读:快照读靠mvcc隐藏新插入行,当前读靠next-key lock阻塞插入;但混合使用两类读仍可能导致逻辑幻读。

RR下幻读到底有没有被解决
MySQL的RR隔离级别对幻读的处理是分场景的:快照读(普通SELECT)靠MVCC“看不见”新插入的行,当前读(带LOCK IN SHARE MODE或FOR UPDATE的SELECT、UPDATE、DELETE)靠next-key lock直接阻塞插入。所以不能笼统说“RR解决了幻读”,而要看你用的是哪种读——两种机制并存,但目标一致:不让事务内两次查询结果集凭空多出或少掉行。
快照读为什么看不到新插入的行
MVCC不是靠锁,而是靠版本可见性判断。每个事务启动时生成一个ReadView,其中包含up_limit_id(活跃事务最小ID)、low_limit_id(下一个将分配的事务ID)等关键信息。对于新插入的行,其DB_TRX_ID必然大于当前事务的up_limit_id,因此不满足“创建版本 ≤ 当前事务版本且删除版本为空或 > 当前事务版本”的可见条件。
常见错误现象:SELECT * FROM users WHERE age >= 25在事务A中执行两次,事务B中途插入age=28的记录并提交,A第二次仍查不到该行——这不是bug,是MVCC按规则过滤的结果。
- 只有主键或唯一索引上的等值查询才可能退化为纯记录锁,范围查询一定会触发间隙/临键锁
-
ReadView在第一次SELECT时生成,后续快照读都复用它,不会随其他事务提交而更新 - 如果事务A先做了
UPDATE再SELECT,InnoDB会升级为当前读,此时MVCC失效,改走锁机制
当前读如何用next-key lock堵住插入
当语句带有加锁子句(如SELECT ... FOR UPDATE)或属于DML操作时,InnoDB会启用next-key lock,即“记录锁 + 间隙锁”的组合。例如SELECT * FROM users WHERE id > 100 FOR UPDATE,实际锁定的是索引中第一个>100的记录及其左侧间隙,形成左开右闭区间,比如(100, 150],新插入id=120就会被阻塞。
容易踩的坑:
- 没有索引的
WHERE条件会导致全表扫描+全表next-key lock,性能雪崩 - 间隙锁之间相互兼容,但会与
INSERT互斥——这意味着并发插入同一大致范围的数据时,事务会排队等待 -
innodb_locks_unsafe_for_binlog = 1会禁用间隙锁,虽提升并发但破坏RR语义,主从不一致风险陡增,生产环境严禁开启
为什么RR不等于Serializable却宣称防幻读
官方文档称RR“防止幻读”,本质是基于SQL标准中对幻读的宽松定义:仅要求同一事务内多次SELECT ... FOR UPDATE结果集不变。而开发者常理解的“两次普通SELECT结果行数不同”在RR下确实可能发生——只要第二次是快照读,第一次是当前读,就属于混合使用模式,不属于标准幻读范畴。
真正要注意的复杂点是:同一个事务里混用快照读和当前读,会导致ReadView和锁机制交叉作用,行为难以预测。比如先SELECT(快照读),再SELECT ... FOR UPDATE(当前读),后者不仅加锁,还会刷新ReadView吗?答案是否定的——ReadView只在首次快照读时生成,但锁范围可能因当前读而扩大,进而影响其他事务。这种隐式耦合,正是RR隔离级别最易被忽略的灰度地带。











