幻读发生在REPEATABLE READ级别下事务两次查询同一范围时,因其他事务在间隙插入新行导致第二次结果多出数据;READ COMMITTED不处理此问题。

幻读到底在什么情况下发生
幻读不是“看到不存在的数据”,而是事务中两次 SELECT 同一范围,第二次多出新插入的行——前提是其他事务在间隙里插了数据。它只在 REPEATABLE READ 隔离级别下被 MySQL 特别处理,READ COMMITTED 下不解决(也不需要锁间隙)。
常见错误现象:SELECT * FROM t WHERE id > 10 AND id 返回 3 行,接着 <code>INSERT INTO t VALUES (15, 'x') 在另一个事务成功执行,再查就变成 4 行——这就是幻读。但注意:如果 id 是主键且值 15 已存在,那不是幻读,是唯一冲突。
- 只有范围条件(
BETWEEN、>、、<code>LIKE 'abc%')才可能触发间隙锁 - 等值查询(
=)且命中唯一索引时,只加记录锁(Record Lock),不锁间隙 - 没索引的字段做范围查询?MySQL 会退化为表锁(或全索引扫描+锁所有间隙),性能灾难
间隙锁(Gap Lock)怎么实际起作用
间隙锁锁住的是两个现有索引记录之间的“空隙”,比如索引有 5 和 10,那么 (5, 10) 这个开区间就被锁住,别人不能往里面插 6、7 等值——但它不阻止 UPDATE 或 DELETE 已存在的记录。
关键点在于:间隙锁是**可共存的**。两个事务可以同时持有同一间隙的 Gap Lock,这避免了死锁,但也意味着它不阻塞“读”,只阻塞“插入”。
- 间隙锁和临键锁(Next-Key Lock)是一体两面:
REPEATABLE READ下默认用 Next-Key Lock(记录锁 + 前面间隙锁),本质是“锁住记录及其左边间隙” -
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE在范围查询时自动加间隙锁;普通SELECT不加 - 唯一索引等值查询(如
WHERE id = 15)不会加间隙锁,哪怕 15 不存在——这时只加“插入意向锁”,冲突时才报错
为什么有时候幻读还是发生了
不是间隙锁失效,而是你根本没走到它生效的路径上。最典型的情况:查询字段没走索引。
比如 SELECT * FROM t WHERE name = 'alice',而 name 没索引,InnoDB 只能走聚簇索引全扫描,此时间隙锁无法精确定界,实际行为接近表级锁定逻辑,但又不完全锁表——结果就是别的事务仍可能在未扫描到的页里插入,导致幻读“漏网”。
- 检查执行计划:
EXPLAIN输出中type是index或range才说明走了索引;ALL就是全表扫 -
INSERT ... SELECT、REPLACE INTO、INSERT ON DUPLICATE KEY UPDATE这些语句也会触发间隙锁,但逻辑更隐蔽,容易误判 - 显式关闭间隙锁?可以设
innodb_locks_unsafe_for_binlog=1(已弃用),或降级到READ COMMITTED,但代价是丢失可重复读语义
线上排查幻读问题该看什么
别急着改 SQL,先确认是不是真幻读,以及锁到底加在哪儿。MySQL 提供了直接观察手段。
运行 SELECT * FROM performance_schema.data_locks(8.0+)或 SHOW ENGINE INNODB STATUS\G 查看当前锁信息。重点关注 LOCK_MODE 字段:含 GAP 就是间隙锁,NEXT-KEY 是临键锁,RECORD 是纯记录锁。
- 用
SELECT ... FOR UPDATE触发后立刻查data_locks,看LOCK_DATA是否显示类似10, 20——表示锁了 (10,20) 这个间隙 - 如果发现大量
WAITING状态的插入被堵住,且等待锁类型是GAP,基本可断定是间隙锁争用 - 注意:间隙锁本身不阻塞
SELECT,所以慢查询日志里看不到它;它只让INSERT/UPDATE卡住
真正难处理的不是锁本身,而是业务逻辑依赖“查无结果就插入”这种模式——它天然和间隙锁冲突。这时候得换思路:用唯一索引约束兜底,靠数据库报 Duplicate entry 错误来判断,而不是靠 SELECT 结果预判。










