间隙锁通过锁定索引中相邻值之间的空隙(如(10,20))来防止幻读,仅在范围查询+当前读且走索引时生效;repeatable read下结合mvcc与next-key lock实现防幻读,read committed则禁用间隙锁。

间隙锁怎么锁定“不存在的行”
间隙锁(Gap Lock)不锁数据行本身,只锁索引中两个值之间的“空档”。比如表里有 id 为 10、20、30 的记录,执行 SELECT * FROM t WHERE id BETWEEN 15 AND 25 FOR UPDATE,InnoDB 会锁定间隙 (10, 20] 和 (20, 30) —— 注意:20 这条记录被记录锁覆盖,而 (10, 20) 和 (20, 30) 这两个“空隙”才是间隙锁的目标。其他事务插入 id=17 或 id=22 就会被阻塞。
关键点在于:间隙锁只在**范围查询 + 当前读**(如 FOR UPDATE、LOCK IN SHARE MODE)时生效;普通 SELECT 不加任何锁,靠 MVCC 快照读避免幻读,但不阻止插入。
- 只有使用索引字段做范围条件时,间隙锁才起作用;全表扫描会退化为表锁
- 唯一索引的等值查询(命中记录)不会加间隙锁,只加记录锁;等值查询(未命中)反而会加间隙锁(例如查
id = 50,但表里最大id是 49,则锁(49, +∞)) - 主键和二级索引都支持间隙锁,但二级索引的间隙锁可能引发更多冲突(因索引顺序 ≠ 主键顺序)
REPEATABLE READ 下为什么一般不会幻读
MySQL 默认隔离级别 REPEATABLE READ 防幻读,靠的是 MVCC + Next-Key Lock 双机制协同:第一次 SELECT 生成一致性快照,后续读仍用该快照(MVCC);而一旦用了 FOR UPDATE 等当前读,InnoDB 自动升级为 Next-Key Lock(记录锁 + 间隙锁),把整个查询范围“封住”。
例如事务 A 执行:SELECT * FROM users WHERE age > 25 FOR UPDATE,它不仅锁住所有 age > 25 的现有记录,还锁住这些记录索引值之间的所有间隙(比如已有 26、30、35,则锁 (25, 26]、(26, 30]、(30, 35]、(35, +∞))。事务 B 插入 age = 28 就会被卡住。
- MVCC 解决“快照内不出现新行”,Next-Key Lock 解决“快照外不让插入新行”——二者缺一不可
- 如果查询没走索引,InnoDB 可能无法精准定位间隙,转而锁整张表(尤其在小表或无索引字段上)
-
READ COMMITTED级别下间隙锁被禁用,此时即使加FOR UPDATE也只锁记录,不锁间隙,幻读风险回归
什么时候间隙锁会失效或被绕过
间隙锁不是万能的,几个典型失效场景必须警惕:
- 事务隔离级别设为
READ COMMITTED:InnoDB 显式关闭间隙锁,只保留记录锁 - 查询条件未命中任何索引(例如
WHERE name LIKE '%abc%'且name无索引):降级为表锁,但间隙锁逻辑不触发 - 插入语句绕过间隙检查:如使用
INSERT ... ON DUPLICATE KEY UPDATE,冲突时更新而非插入,不触发间隙锁等待 - 自增主键的插入:InnoDB 对
auto_increment列有特殊优化,间隙锁对其插入行为约束较弱(尤其在批量插入时)
一个常见误判是:看到 SELECT ... FOR UPDATE 没阻塞插入,就认为“间隙锁没生效”。其实更可能是查询没走索引,或者隔离级别被意外改成了 READ COMMITTED。
要不要升到 SERIALIZABLE 来防幻读
不用。除非你确认业务能接受极低并发吞吐——因为 SERIALIZABLE 会让所有 SELECT 隐式变成 SELECT ... LOCK IN SHARE MODE,连普通读都要加共享锁,写操作极易排队等待。
真实项目中,更务实的做法是:保持默认 REPEATABLE READ,对关键范围操作显式加锁,并确保查询字段有合适索引。比如资金流水按时间范围查并扣减,就用 SELECT ... WHERE created_at > '2026-01-01' FOR UPDATE,同时给 created_at 建索引。
-
SERIALIZABLE在高并发写场景下,锁等待超时(Lock wait timeout exceeded)会非常频繁 - 它不能解决所有一致性问题——比如跨表关联查询,仍需应用层加锁或重试逻辑
- 真正难处理的幻读,往往不在单表范围查询,而在业务逻辑跨越多个事务步骤时(例如先查再判再插),这时光靠数据库锁不够,得结合应用层幂等或状态机
间隙锁的边界感很关键:它只保护索引结构上的“空隙”,不保护业务语义上的“逻辑空白”。比如用户注册限制“每小时最多 10 次”,这个“小时窗口”就得靠应用层计数器或 Redis 原子操作,数据库间隙锁帮不上忙。










