幻读是同一事务两次范围查询结果行数不一致,因其他事务插入新行所致;mysql的rr级别下快照读不显幻读,但当前读会暴露新行;防幻读需用next-key lock、serializable或应用层唯一约束。

幻读到底是什么现象?
幻读不是数据“变脏”或“被改”,而是同一个事务里,两次执行一模一样的范围查询(比如 SELECT * FROM orders WHERE status = 'pending'),第二次却多出了一行——这行是别的事务在两次查询之间 INSERT 进来的。它像幽灵一样“凭空出现”,所以叫“幻读”。关键点:不是同一行被改了(那是不可重复读),而是**行数变了**。
为什么可重复读(RR)隔离级别下还会幻读?
MySQL 默认的 REPEATABLE READ 确实靠 MVCC 实现快照读,能挡住其他事务的 UPDATE/DELETE 对已存在行的影响,但对新插入的行无感——因为新行的 trx_id 比当前事务启动时的 snapshot 版本号还大,MVCC 不会把它纳入快照。所以:
• 普通 SELECT(快照读):看不到新插入的行 → 表面没幻读
• 但一旦你执行 SELECT ... FOR UPDATE 或 INSERT ... SELECT 这类当前读,InnoDB 就必须查真实索引结构 → 新行就暴露了 → 幻读立刻发生
怎么真正拦住幻读?三个实操手段
1. 加 Next-Key Lock(最常用)
• 在范围条件上加锁,比如 SELECT * FROM t WHERE id > 100 FOR UPDATE,InnoDB 不仅锁住已有记录,还会锁住 (100, +∞) 这个间隙
• 要求字段有索引(主键或二级索引),否则退化为表锁
2. 升级到 SERIALIZABLE
• MySQL 会自动把所有普通 SELECT 改成 SELECT ... LOCK IN SHARE MODE
• 并发急剧下降,线上慎用
3. 应用层配合(推荐高频场景)
• 比如“先查再插”逻辑,不用 SELECT ... INSERT,改用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE
• 避免依赖两次查询的一致性,直接靠唯一约束兜底
最容易被忽略的坑
• SELECT * FROM t WHERE name = 'xxx' —— 如果 name 没索引,InnoDB 无法加间隙锁,幻读照常发生
• UPDATE 语句只锁匹配到的行,不锁间隙;如果想防幻读,得显式加 FOR UPDATE 或改写为带范围条件的锁读
• READ COMMITTED 下连 MVCC 快照都每条语句刷新一次,幻读比 RR 更频繁,别误以为“读已提交更安全”










