复合索引非前导列更新触发间隙锁,因未满足最左前缀原则,优化器无法精确定位,被迫全索引分支扫描,进而对扫描路径上所有间隙加Next-Key Lock;例如索引(idx_user_status)中仅WHERE status='pending',若强制走索引,则按user_id维度横向扫描,锁住如(100,200]、(200,300]等区间,锁范围远超单列索引。

复合索引非前导列更新为什么触发间隙锁
因为InnoDB加锁只认最左前缀,WHERE条件没命中索引开头字段时,优化器无法精确定位数据范围,只能退化为扫描整个索引分支,进而对扫描路径上的所有间隙加锁。
UPDATE t SET x=1 WHERE status = 'pending' 走了复合索引 idx_user_status 会锁哪些范围
假设索引是 KEY idx_user_status (user_id, status),而查询只用 status = 'pending',那这个查询根本不会走该索引——除非你强制 FORCE INDEX。但一旦强制走,InnoDB会遍历整个索引树中所有 user_id 分支,只要该分支下存在 status = 'pending' 的叶子节点,就可能对整段 user_id 区间加 Next-Key Lock。
- 实际锁住的不是 status 值本身,而是索引项中所有满足
status = 'pending'的(user_id, status)组合所处的 B+ 树位置及其左右间隙 - 例如索引中有 (100, 'pending'), (200, 'pending'), (300, 'pending'),InnoDB 可能锁定 (100, 200] 和 (200, 300] 两个区间,哪怕你只更新其中一行
-
EXPLAIN FORMAT=tree会显示type: index或type: ALL,key_len显示为 0 或极小值,说明未利用最左前缀
为什么比单列非唯一索引锁得更宽
单列非唯一索引(如 KEY idx_status (status))至少能按 status 值聚簇,锁范围相对可控;而复合索引非前导列更新,等于放弃索引的有序性主干,被迫在多个 user_id 子树中横向扫描,间隙覆盖维度从一维升到二维甚至更高。
- 锁粒度从「某个 status 值对应的所有行 + 其间隙」变成「所有 user_id 下可能含该 status 的任意子区间」
- 若
user_id是递增主键,那扫描可能横跨几十万行的主键范围,间隙锁直接覆盖 (min_id, max_id) 整个大段 - 并发事务各自按不同非前导列条件更新,极易在重叠间隙上形成插入意向锁冲突,死锁日志里常出现
lock_mode X locks gap before rec
怎么验证和收缩这种扩大化的间隙锁
别猜,用 performance_schema.data_locks 看真实锁范围;收缩的关键不是改 SQL,而是重构索引或访问路径。
- 执行后立刻查:
SELECT ENGINE_TRANSACTION_ID, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_DATA FROM performance_schema.data_locks WHERE OBJECT_SCHEMA = 'db' AND OBJECT_NAME = 't'; - 如果必须按
status更新,就把status单独建唯一索引(或加UNIQUE约束),让等值更新退化为纯 record lock - 若业务允许,先用主键查出 ID 列表(
SELECT id FROM t WHERE status = 'pending' LIMIT 100),再用UPDATE ... WHERE id IN (...)分批更新——这样只锁主键,无间隙锁 - 避免
FORCE INDEX强行走复合索引非前导列,那是把锁问题从“隐式”变“显式爆炸”


















