SQL 幻读是如何产生的?

10次阅读

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

SQL 幻读是如何产生的?

幻读的本质是「范围查询 + 新行插入」

幻读不是数据被改了,而是你查的“那一片”里,突然多了一条或少了条记录。比如事务 A 执行 SELECT * FROM users WHERE age > 30 得到 5 条,事务 B 插入一条 age = 35 的新用户并提交,A 再查就变成 6 条——这多出来的那条,就是“幻影行”。关键点在于:它只发生在范围条件(>BETWEENLIKE '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 是关键:它强制走当前读,并激活间隙锁。漏掉它,或者用错隔离级别,幻读就会在高并发下真实出现——而且往往只在压测或上线后才暴露,因为它是概率性竞争问题。

text=ZqhQzanResources