非唯一索引+范围查询会锁住大片间隙,因InnoDB在RR级别下默认使用Next-Key Lock,需按索引顺序扫描至首个不满足条件的记录,导致锁覆盖整个扫描区间,即使中间值不存在。

为什么非唯一索引 + 范围查询会锁住一大片间隙
MySQL 在 RR 隔离级别下,对非唯一索引执行范围查询(如 WHERE status BETWEEN 0 AND 2)时,默认使用 Next-Key Lock,也就是「记录锁 + 左侧间隙锁」的组合。它不是只锁住查到的几行,而是锁住整个扫描覆盖的索引区间——哪怕中间大量值根本不存在。
关键原因在于:非唯一索引无法精确定位边界,InnoDB 必须按索引顺序向右延伸,直到碰到第一个不满足条件的记录为止。这个“扫描终点”可能远超业务预期。
常见现象包括:
-
SELECT ... FOR UPDATE WHERE status IN (0,1)实际锁住status=0到下一个status > 1的所有间隙,比如status=5、status=100所在的间隙也被锁 - 表中
status值只有0,1,3,但WHERE status 仍会锁住 <code>(1,3)这个间隙,阻止插入status=2 - 如果该非唯一索引上
status最大值是1,那WHERE status > 0可能锁住从1到正无穷的整个后缀间隙
这和唯一索引完全不同:WHERE id = 5(主键或唯一索引)只会加 Record Lock;而 WHERE status = 5(非唯一索引)即使只命中一行,也会加 Next-Key Lock,锁住该值对应记录及其左侧间隙。
如何快速确认当前 SQL 锁了哪些间隙
别猜,直接看 INFORMATION_SCHEMA.INNODB_TRX 和 INFORMATION_SCHEMA.INNODB_LOCKS(MySQL 8.0.24+ 已废弃,改用 performance_schema.data_locks):
最稳的方法是复现后立即执行:
SELECT * FROM performance_schema.data_locks\G
重点关注字段:
-
LOCK_TRX_ID:关联事务 ID -
LOCK_MODE:看到GAP、NEXT-KEY就说明涉及间隙 -
LOCK_DATA:显示被锁的索引值(如'1')或间隙边界(如'1', '3'表示锁(1,3)) -
LOCK_INDEX:确认是不是你怀疑的那个非唯一索引
配合 SHOW ENGINE INNODB STATUS\G 中的 LATEST DETECTED DEADLOCK 或 TRANSACTIONS 部分,能交叉验证锁范围是否异常膨胀。
非唯一索引范围查询的三个高危配置点
-
WHERE条件没走索引:比如对非唯一索引字段用了函数WHERE UPPER(status) = 'PAID',会导致全表扫描 → 全表间隙都被锁(等效于锁表) - 索引选择性差:
status只有 3 个值(0/1/2),但有百万行,优化器可能放弃走索引 → 同样引发大范围扫描加锁 - 范围过大且无 LIMIT:例如
UPDATE t SET x=1 WHERE status > 0,若status > 0匹配 90% 行数,InnoDB 会锁住从第一个status > 0记录开始,一直到索引末尾的所有间隙
实际优化建议:
- 把高频范围查询的非唯一索引,升级为联合索引并把范围字段放在最后(如
(tenant_id, status)),利用等值前缀缩小扫描范围 - 明确需要「仅锁命中的行」时,改用
READ-COMMITTED隔离级别(此时 InnoDB 不用间隙锁,但需接受幻读) - 批量更新务必加
LIMIT并分页,避免单次事务锁住过多间隙
容易被忽略的隐式锁扩大行为
ORM 框架(如 MyBatis、GORM)生成的动态 SQL,常因参数为空跳过条件,导致原本带 AND status = ? 的语句变成纯 WHERE id = ? —— 看似安全,但如果这个 id 是主键,而执行计划里因统计信息陈旧误判为需要回表查二级索引,就可能触发对二级索引的范围扫描 + 间隙锁。
更隐蔽的是:
-
ORDER BY字段不在索引中,强制 filesort → 可能让优化器放弃使用部分索引,扩大扫描范围 -
OR条件拆分后,各分支走不同索引,最终合并结果集时,InnoDB 会对每个扫描路径分别加锁,总锁范围是并集
所以线上一旦出现间隙锁范围过大,第一反应不该是“加索引”,而是先用 EXPLAIN FORMAT=JSON 看真实执行计划,确认是否真的走到了你认为的那个非唯一索引,以及扫描行数预估是否合理。











