间隙锁是innodb在可重复读隔离级别下为防止幻读而锁定索引间隙的机制,仅在当前读、走索引、且满足范围查询或不存在记录的等值查询时触发,锁区间为左开右开(如(10,20)),阻止其他事务向该空白区间插入数据。

间隙锁不是“锁某条记录”,而是锁住索引中两个值之间的空白区间,目的是阻止其他事务往这个空档里插入新数据——它只在可重复读(REPEATABLE READ)隔离级别下生效,且必须配合当前读操作触发。
间隙锁触发的三个硬性条件
缺一不可,否则根本不会出现间隙锁:
- 事务隔离级别必须是
REPEATABLE READ(MySQL 默认),READ COMMITTED下间隙锁基本失效(唯一索引等值查询除外) - SQL 必须走索引——全表扫描或没索引的查询会退化成表锁,不产生间隙锁
- 必须是当前读操作:
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE;普通SELECT(快照读)完全不加任何间隙锁
哪些 SQL 语句实际会触发间隙锁?
常见但容易误判的场景:
SELECT * FROM users WHERE age > 10 AND age :范围查询,锁住 (10,20) 区间内所有可能插入的位置-
SELECT * FROM users WHERE id = 100 FOR UPDATE(id 是唯一索引,但 100 不存在):查不到记录,InnoDB 会锁定 (prev_id, next_id) 这个间隙,比如前一条是 95、后一条是 105,就锁 (95,105) -
SELECT * FROM users WHERE age BETWEEN 25 AND 30 FOR UPDATE(age 是普通索引):即使只命中一条记录,也会锁住该值前后相邻的间隙,形成临键锁(next-key lock) -
INSERT INTO users (id, age) VALUES (50, 28):插入本身会触发对目标位置的间隙检查,若该位置已被间隙锁覆盖,就会阻塞
为什么有时候“明明没查到数据,却卡住了”?
这是间隙锁最典型的副作用表现:
- 事务 A 执行
SELECT * FROM t WHERE age = 28 FOR UPDATE,而表中没有 age=28 的记录 → InnoDB 锁住 (25,30) 这个间隙(假设索引中最近的上下界是 25 和 30) - 事务 B 尝试执行
INSERT INTO t (age) VALUES (28)→ 插入点落在 (25,30) 内,被事务 A 的间隙锁阻挡,一直等待 - 事务 B 并不是在等“某行被锁”,而是在等“某个空位置被放开”——这种等待无法通过
SHOW ENGINE INNODB STATUS直观看到具体哪行,只能看到 waiting for gap lock - 注意:如果 age 字段没有索引,这条
SELECT ... FOR UPDATE会直接升级为表锁,而不是间隙锁,行为完全不同
间隙锁和临键锁的区别与混淆点
很多人把它们当成两种锁,其实临键锁是间隙锁 + 记录锁的组合体,本质仍是间隙锁机制的延伸:
- 对存在记录的等值查询(如
SELECT ... WHERE id = 5 FOR UPDATE,id 是主键且 5 存在)→ 加的是记录锁(record lock),不涉及间隙 - 对存在记录的范围查询(如
SELECT ... WHERE age > 20 FOR UPDATE)→ 加的是临键锁,即锁住每个匹配记录本身 + 它右边的间隙 - 对不存在记录的等值查询(如
SELECT ... WHERE age = 22 FOR UPDATE,但 age=22 不存在)→ 只加间隙锁,不锁任何记录 - 间隙锁本身永远是左开右开(如 (10,20)),但 InnoDB 内部描述常按左开右闭(如 (10,20])呈现,实际效果一致:不允许在该区间插入任何新值
真正容易被忽略的,是“索引类型”和“是否存在对应记录”这两个变量共同决定锁类型——同一句 SQL,在不同数据分布下,可能加记录锁、间隙锁或临键锁,不能只看写法。调试时务必用 EXPLAIN 确认是否走了索引,并用 SELECT * FROM performance_schema.data_locks 查看实际加了什么锁。











