普通索引等值查询必然加间隙锁,因其需向右遍历确认唯一性,next-key lock退化为gap lock;范围查询则持续加next-key lock至首个不满足条件记录或supremum。

普通索引加锁范围不能只看 WHERE 条件值,得结合「是否命中记录」「是否向右遍历」「是否覆盖索引」三件事一起判断。结论很直接:普通索引等值查询必然带间隙锁,且锁范围常超出查询值本身。
普通索引等值查询为什么一定有间隙锁?
因为 next-key lock 是 InnoDB 加锁基本单位,而普通索引(非唯一)无法通过一次定位确认“后面还有没有相同值”,所以必须向右扫描直到遇到第一个不满足条件的值——这个过程会让锁扩散到后续间隙。
常见错误现象:select * from t where c = 5 for update 执行后,insert into t (c) values (7) 被阻塞,但 c=5 行明明只有一条——这是因为锁了 (5,10) 这个间隙(假设下一条 c=10)。
- 只要
c是普通索引,哪怕只有一行匹配,也会先加next-key lock (x,5],再向右走到c=10,退化为gap lock (5,10) - 如果
c=5根本不存在,那只会加gap lock (x,5)(前一个值到 5 之间) - 若查询用的是覆盖索引(比如
select c from t where c=5 for update),主键索引上不加锁,但普通索引上该有的next-key和gap锁一个不少
普通索引范围查询(如 >=)怎么算锁范围?
范围查询不会退化成纯记录锁,next-key lock 基本保持原样,且会一直扫到“第一个不满足条件的记录”才停——这个边界决定间隙锁终点。
使用场景举例:select * from t where c >= 5 for update,假设索引顺序是 c: 1,5,8,12:
- 从
c=5开始,加next-key lock (1,5]→ 实际是记录锁(因c=5存在) - 继续向右,
c=8满足条件,加next-key lock (5,8] - 继续,
c=12也满足,加next-key lock (8,12] - 再往后没有记录了,但会锁
supremum(上界伪值),即加next-key lock (12,supremum]
最终锁住所有匹配行 + 它们之间的间隙 + 最大值之后的间隙。性能影响明显:范围越宽、数据越稀疏,锁住的间隙越大,插入冲突概率越高。
为什么 force index 不能绕过普通索引的间隙锁?
显式指定 force index(idx_c) 只是让优化器走这个索引,但不改变加锁逻辑——锁还是加在 idx_c 上,且依然遵循“访问到的索引项都要锁”原则。
容易踩的坑:
-
select * from t force index(idx_c) where c = 5 for update和不加force效果完全一样,锁范围不变 - 如果
idx_c是联合索引(如(c,d)),where c = 5仍是等值查询,但会扫描所有c=5的d值,锁范围取决于d的实际分布 - 误以为「用了索引就只锁一行」——实际上普通索引等值查询最少锁两个区间:
(prev,5]+(5,next)
如何验证当前语句到底锁了哪些范围?
别猜,查 performance_schema.data_locks ——这是唯一可靠方式。注意字段含义:
-
INDEX_NAME显示锁在哪张索引上(PRIMARY或具体索引名) -
LOCK_DATA显示锁的具体值或区间,例如5(记录锁)、5, 10(间隙锁)、5, 10后跟SUPREMUM(上界锁) -
LOCK_MODE里X,REC_NOT_GAP是记录锁,X,GAP是间隙锁,X,ORDINARY是临键锁
复杂点在于:同一语句可能在多个索引上同时加锁(比如普通索引 + 主键),而 data_locks 会逐条列出。最容易被忽略的是——**间隙锁不对应任何真实行,却能阻塞插入;它只在 LOCK_DATA 里以区间形式存在,不在表中可见**。











