非唯一索引范围查询锁住(a,b)和(b,c)两个间隙,是因为innodb必须用next-key lock覆盖整个扫描路径:非唯一索引无法唯一定位终点,需向右多扫一个索引项以防遗漏重复值,故锁定包括(20,25)、(25,25)、(25,30)、(30,35)等所有相关间隙,确保幻读防控。

非唯一索引范围查询为何锁住 (a, b) 和 (b, c) 两个间隙?
因为 InnoDB 必须用 Next-Key Lock 覆盖整个扫描路径,而不仅仅是匹配行。非唯一索引无法通过键值唯一定位终点,所以会向右“多扫一个”,把本不该锁的下一个索引项也纳入锁定范围。
比如 SELECT * FROM t WHERE age BETWEEN 25 AND 30 FOR UPDATE,若索引中 age 值为 20, 25, 25, 30, 35:
- 它会锁定所有
age = 25和age = 30的记录(Record Lock) - 同时锁定
(20, 25)、(25, 25)、(25, 30)、(30, 35)四个间隙(Gap Lock) - 其中
(25, 25)是重复键值之间的“虚拟间隙”,也会被锁
这不是 bug,而是防止幻读的强制行为:只要索引键可能重复,InnoDB 就不敢在扫描到第一个不满足条件的键时停下。
为什么唯一索引范围查询能退化,而非唯一索引不能?
唯一索引的键值天然排他,InnoDB 可以精确判断边界;非唯一索引不行——哪怕当前只查到 age = 30,也不能排除下一个 age = 30 还在后面,必须继续探查。
关键差异点:
-
WHERE id > 100(主键):扫描到第一个id = 101后,若下一个是id = 105,则只锁(100, 101]和(101, 105],不会盲目延伸 -
WHERE age > 25(非唯一索引):即使当前页最大age = 30,InnoDB 仍要确认后续页是否还有age = 25或age = 26,于是锁住(30, +∞) - EXPLAIN 显示
type为range且key非空,不代表锁范围小;要看实际索引分布和重复密度
如何验证你正被非唯一索引的间隙锁拖慢?
最直接的方式是观察阻塞链和执行计划:
- 用
SELECT * FROM performance_schema.data_locks查当前事务持有的锁,重点关注LOCK_MODE含GAP或NEXT-KEY的行 - 运行
EXPLAIN FORMAT=TRADITIONAL SELECT ... FOR UPDATE,如果rows显著大于实际匹配数,大概率已锁多行+多间隙 - 模拟插入:另起事务执行
INSERT INTO t (age, ...) VALUES (26, ...),若被阻塞,说明(25, 30)间隙已被锁 - 注意:
SHOW ENGINE INNODB STATUS\G中的TRANSACTIONS段会显示等待的锁类型和持锁事务
哪些写法会让非唯一索引的锁范围雪上加霜?
不是所有 BETWEEN 或 > 都一样危险,这些写法会显著扩大间隙覆盖:
- 用函数包裹字段:
WHERE YEAR(create_time) = 2024→ 索引失效 → 全表扫描 → 所有主键 next-key 锁 - 隐式类型转换:
WHERE age = '25'(age 是 INT)→ 索引失效 → 同上 - 复合索引未按最左前缀使用:
WHERE status = 'done'在(tenant_id, status)索引上 → 只能全索引扫描 → 锁整个二级索引树 - ORDER BY 非索引列 + LIMIT:
WHERE age > 20 ORDER BY name LIMIT 10→ 无法提前终止扫描 → 锁更多间隙
真正难缠的不是锁本身,而是你以为只锁了几行,结果发现插入、更新都被卡在看似无关的间隙上——这种延迟往往没有明显报错,只表现为偶发性超时或长事务阻塞。











