mysql的repeatable-read隔离级别本身不解决幻读,innodb依赖next-key lock(当前读+索引+范围查询)兜底,但快照读不加锁、等值查询只加记录锁、无索引时锁失效;混用快照读与当前读是幻读主因,需统一用select ... for update或应用层兜底。

RR下幻读没被“自动消灭”,只在当前读+索引+范围查询时才防得住
MySQL的REPEATABLE-READ隔离级别本身不解决幻读,InnoDB靠Next-Key Lock(临键锁)来兜底,但这机制有严格触发条件:必须是当前读、必须走索引、必须是范围条件。普通SELECT(快照读)完全不加锁,也不管间隙;等值查询如WHERE id = 100只加记录锁,不锁间隙;没索引或LIKE '%abc'这类无法走索引的条件,InnoDB会退化为表锁或干脆不锁间隙。
常见错误现象:事务A先SELECT * FROM users WHERE status = 'active'(快照读,看到10行)→事务B插入一条status = 'active'的新用户并提交→事务A再执行UPDATE users SET version = version + 1 WHERE status = 'active'(当前读,影响11行)。这不是bug,是设计使然:两次读语义不同,结果集天然可能不一致。
-
SELECT ... FOR UPDATE和UPDATE才触发Next-Key Lock,普通SELECT永远不锁 - 主键等值查询只锁单行,不封间隙,别人仍可插在前后(比如id=5被锁,id=4或6仍可插入)
- 二级索引若非唯一且查询条件不覆盖全部索引列,间隙锁可能漏掉部分区间
混用快照读和当前读是幻读高发场景
业务代码里最常踩的坑,就是同一个事务内先做一次普通SELECT判断是否存在,再用INSERT ... ON DUPLICATE KEY UPDATE或UPDATE修改——这看似安全,实则暴露幻读。因为第一次SELECT基于Read View,第二次UPDATE穿透MVCC读最新数据并加锁,中间插入的新行对前者不可见,对后者可见。
典型表现:事务A查SELECT COUNT(*) FROM orders WHERE user_id = 123得0 → 事务B插入user_id = 123的订单并提交 → 事务A执行INSERT INTO orders (...) VALUES (...)(无唯一冲突)→ 最后事务A再SELECT发现多出1行。问题不在RR级别失效,而在读模式切换导致视图不一致。
- 避免在同一事务中交替使用
SELECT和SELECT ... FOR UPDATE - 如果逻辑需要“先查后更”,统一改用
SELECT ... FOR UPDATE开头,把锁范围一次性建立好 - 不要依赖快照读结果做决策依据,尤其涉及计数、存在性校验等场景
间隙锁不是万能的,它受索引结构和查询条件双重制约
Next-Key Lock本质是“记录锁+间隙锁”的组合,但它只在B+树索引的扫描路径上生效。没有索引,InnoDB只能全表扫描,此时间隙锁退化为行锁甚至失效;索引存在但查询条件无法利用(如WHERE JSON_CONTAINS(meta, '"paid"')),同样不触发间隙锁。
实操中容易忽略的点:WHERE age > 25会锁住(25, +∞)这个右开区间,但WHERE age >= 25在有索引时行为相同;而WHERE age = 25哪怕有索引,也只锁id=25这一行及其左右间隙(即(24,25]和(25,26]),并不封死整个范围。如果你真正想防的是“所有age=25的插入”,那必须确保age字段上有唯一索引,否则别人仍可插多个age=25的行(除非你用INSERT ... ON DUPLICATE KEY UPDATE配合唯一约束)。
- 检查
EXPLAIN输出,确认你的WHERE条件确实走了索引 - 范围查询优先用
BETWEEN或> / ,避免<code>IN列表过大导致优化器放弃范围扫描 - 联合索引要注意最左匹配原则,
WHERE a = 1 AND b > 10能用上(a,b),但WHERE b > 10就用不上
SERIALIZABLE也不是银弹,它只是把幻读问题显式抛给应用层
把隔离级别设成SERIALIZABLE确实会让所有SELECT自动转为SELECT LOCK IN SHARE MODE,从而统一当前读语义、彻底规避幻读。但代价是并发性能断崖式下跌——同一范围的读写操作极易相互阻塞,小流量尚可,高并发下单表写入可能直接卡死。
更现实的做法是:识别出真正需要强一致的业务路径(比如库存扣减、账户余额更新),在这些关键事务里主动用SELECT ... FOR UPDATE加锁,并确保WHERE条件走索引;其余读多写少的场景,接受快照读带来的“幻读感”,用应用层幂等或最终一致性来兜底。
真正容易被忽略的复杂点,不是“怎么加锁”,而是“什么时候不该加锁”——比如分页查询SELECT * FROM log ORDER BY id LIMIT 100 OFFSET 10000,加FOR UPDATE不仅没意义,还会拖慢整个系统。幻读防御必须和具体业务语义绑定,脱离场景谈隔离级别,等于在真空里调优。











