幻读本质是范围查询时新行插入导致结果集变化;mysql在REPEATABLE READ级别通过Gap Lock锁定索引间隙防止幻读,但无索引字段、READ COMMITTED级别或普通快照读仍可能触发幻读。

幻读的本质是「范围查询 + 新行插入」
幻读不是数据被改了,而是你查的“那一片”里,突然多了一条或少了条记录。比如事务 A 执行 SELECT * FROM users WHERE age > 30 得到 5 条,事务 B 插入一条 age = 35 的新用户并提交,A 再查就变成 6 条——这多出来的那条,就是“幻影行”。关键点在于:它只发生在范围条件(>、BETWEEN、LIKE 'a%')下,精确匹配(WHERE id = 100)不会触发幻读。
MySQL 默认的 REPEATABLE READ 为什么能防幻读?
因为 InnoDB 在可重复读级别下,不只是锁住查到的行,还会用 Gap Lock(间隙锁)把查询范围的“空隙”也锁住。比如索引上有值 10、20、30,WHERE id BETWEEN 15 AND 25 不仅锁住 20 这一行,还会锁住 (10,20) 和 (20,30) 这两个间隙,阻止别人往里面插 18 或 22。但注意:间隙锁只在有索引的列上生效;全表扫描或无索引字段上的范围查询,间隙锁会退化为临键锁甚至失效。
哪些操作实际不防幻读?容易踩的坑
-
select普通快照读(不加锁):即使在 RR 级别,它也只读 MVCC 快照,完全感知不到新插入的行——这不是 bug,是设计使然 - 使用
READ COMMITTED隔离级别:间隙锁被禁用,每次查询都读最新已提交版本,幻读必然发生 - 主键/唯一索引上的等值查询(如
WHERE id = ?):只会加行锁,不加间隙锁,但此时插入同 id 会因唯一约束失败,所以“幻读感”不明显;而WHERE name = ?(name 无索引)则可能直接全表扫+无间隙保护 - 显式用了
SELECT ... LOCK IN SHARE MODE却没覆盖完整范围:比如只锁了部分索引段,间隙仍可插入
真正要防幻读,得靠当前读 + 正确加锁
如果你的业务逻辑依赖“查出来没有,我就插入”,比如防重复下单,就不能只靠 SELECT 判断,必须用当前读锁定范围:
START TRANSACTION; SELECT * FROM orders WHERE user_id = 123 AND status = 'pending' for UPDATE; -- 此时不仅锁住现有行,还锁住 (prev_id, next_id) 间隙 INSERT INTO orders (...) VALUES (...); -- 安全插入,不会冲突 COMMIT;
这里 FOR UPDATE 是关键:它强制走当前读,并激活间隙锁。漏掉它,或者用错隔离级别,幻读就会在高并发下真实出现——而且往往只在压测或上线后才暴露,因为它是概率性竞争问题。