MySQL 8.0的RR隔离级别不能完全避免幻读,因其仅在当前读+索引+范围查询三者同时满足时才通过Next-Key Lock有效防御;快照读复用ReadView不锁间隙,混用两种读模式会导致语义冲突与幻读暴露。

MySQL 8.0 的 RR 隔离级别**不能完全避免幻读**,不是因为实现有缺陷,而是它对“幻读”的防御是有明确边界的:只在**当前读 + 索引 + 范围查询**三者同时满足时才生效;其余场景下,幻读天然可能发生。
快照读不锁间隙,但业务常拿它做决策
事务启动后第一次 SELECT 生成 ReadView,后续所有普通 SELECT 都复用这个快照——新插入且已提交的行(DB_TRX_ID > max_trx_id)直接不可见。所以两次 SELECT * FROM t WHERE b = 1 结果一致,看起来没幻读。
但问题出在业务逻辑依赖这个“一致结果”做判断:
- 事务 A 先
SELECT COUNT(*) FROM orders WHERE user_id = 123得到 0 - 事务 B 插入
user_id = 123的订单并提交 - 事务 A 接着执行
INSERT INTO orders (...) VALUES (...)(无唯一冲突)成功 - 最后事务 A 再
SELECT发现多出 1 行
这不是数据错乱,是语义错配:快照读回答“当时有没有”,而 INSERT 本质是当前读(需检查约束、生成新版本),它看到的是最新状态。
Next-Key Lock 只在特定当前读中触发
SELECT FOR UPDATE、UPDATE、DELETE 这些当前读语句才可能加 Next-Key Lock,但它生效要同时满足:
-
WHERE条件必须走索引(主键、唯一索引、普通二级索引均可,但LIKE '%abc'或无索引字段会退化为全表扫描) - 必须是范围条件(如
WHERE id > 10、WHERE b BETWEEN 1 AND 5);等值查询WHERE b = 1在非唯一索引上才锁间隙,唯一索引上只加记录锁 - 若事务 B 在事务 A 执行当前读之前就插入并提交了新行,Next-Key Lock 不会回滚它,只阻塞后续插入
例如:UPDATE t SET c = 1 WHERE a = 100,若 a 是唯一索引,InnoDB 只锁那一条记录,不封间隙——别人仍可插入另一个 a = 100(违反唯一约束会报错,但若 a 不唯一,就真插进去了)。
混用快照读和当前读是幻读高发路径
这是业务代码里最常踩的坑,典型模式:
- 先
SELECT ... WHERE status = 'pending'判断是否存在待处理任务 - 再
UPDATE ... SET status = 'processing' WHERE status = 'pending' LIMIT 1
第一次是快照读(看不到新插入的 pending),第二次是当前读(能看到并更新),中间插入的新行对前者不可见、对后者可见——结果就是“漏处理”或“重复处理”。
解决方式不是升级隔离级别,而是统一读取模式:
- 所有需要强一致性的判断,开头就用
SELECT ... FOR UPDATE,把锁范围一次性建立好 - 避免同一事务内交替使用普通
SELECT和FOR UPDATE - 不要依赖快照读结果做存在性校验、计数、分页总数等决策依据
真正容易被忽略的一点是:Next-Key Lock 锁的是 B+ 树索引上的物理区间,不是你业务理解的“逻辑范围”。比如索引值当前是 [1, 2, 4],WHERE b > 2 锁的是 (2, 4] 和 (4, +∞),但如果有人在 (2, 4) 之间插入 b=3,会被拦住;而如果插入 b=2.5 且该列是字符串或没走索引,锁就失效了。


















