rr级别下主键等值查询会触发间隙锁,当且仅当目标主键值不存在时,innodb为防止幻读而在“本该存在”的位置加间隙锁;命中真实记录则只加记录锁。

RR 级别下主键查询为什么还会触发间隙锁?
MySQL 在可重复读(RR)隔离级别下,即使你用 WHERE id = 123 这种明确的主键等值查询,也可能加间隙锁——前提是这个主键值**不存在**。InnoDB 的加锁逻辑不是“看你怎么查”,而是“查到了什么、没查到什么”。如果 SELECT ... FOR UPDATE 或 UPDATE 扫描时发现目标记录不存在,它就会在“本该存在”的位置前后加间隙锁,防止其他事务插入相同主键值(即防止幻读)。
常见错误现象:Deadlock found when trying to get lock 出现在两个事务都执行 SELECT * FROM t WHERE id = 100 FOR UPDATE,而 id = 100 当前不存在。
- 只有
SELECT ... FOR UPDATE或UPDATE/DELETE会加锁;普通SELECT不会 - 主键必须是聚簇索引(InnoDB 默认满足),且查询条件是**精确匹配主键列**(不能是函数、表达式或隐式转换)
- 如果主键是
INT类型,但传入字符串'100',可能触发类型转换,导致走全表扫描或索引失效,进而扩大锁范围
怎么确认当前查询到底加了什么锁?
靠猜不行,得看 INFORMATION_SCHEMA.INNODB_TRX 和 INFORMATION_SCHEMA.INNODB_LOCKS(MySQL 5.7)或 performance_schema.data_locks(MySQL 8.0+)。最直接的方式是复现死锁后查 SHOW ENGINE INNODB STATUS,里面 LATEST DETECTED DEADLOCK 段会明确写出每个事务持有的锁和等待的锁。
- MySQL 8.0 推荐用:
SELECT * FROM performance_schema.data_locks,重点关注LOCK_MODE(如X,GAP表示间隙锁)和LOCK_DATA(显示间隙的边界值) - 如果
LOCK_DATA显示100,但表里没有id = 100的行,基本可以断定是间隙锁卡在 (99, 101) 区间 - 注意:
SELECT ... LOCK IN SHARE MODE同样会加间隙锁,行为和FOR UPDATE一致
用主键查询避免间隙锁的三个硬条件
想让主键等值查询完全不加间隙锁,只加记录锁(Record Lock),必须同时满足:
- 查询必须命中**真实存在的记录**(不能是“查无此 ID”)
- 查询条件必须是**独立的主键列等值判断**,不能带
OR、不能有IS NULL、不能和其他非主键条件组合(比如WHERE id = 100 AND status = 1可能导致优化器放弃主键索引) - 事务中不能先执行过范围扫描(如
WHERE id > 90),否则后续对id = 100的操作可能被已有的间隙锁影响
示例对比:
-- ✅ 安全:id 是主键,值存在,条件干净 SELECT * FROM t WHERE id = 100 FOR UPDATE; <p>-- ❌ 危险:id=100 不存在 → 加 (99,101) 间隙锁 SELECT * FROM t WHERE id = 100 FOR UPDATE;</p><p>-- ❌ 危险:看似主键查询,但优化器可能走错索引 SELECT * FROM t WHERE id = 100 AND deleted = 0 FOR UPDATE;</p>
死锁高频场景:插入前校验 + FOR UPDATE 的经典陷阱
这是业务中最容易踩坑的模式:先查是否存在,不存在就插入。如果两个事务几乎同时执行 SELECT ... FOR UPDATE 查一个不存在的主键,都会在相同间隙上加锁,接着又都想插入,就形成循环等待。
- 根本原因不是“用了 FOR UPDATE”,而是“对不存在的主键加锁”+“后续要 INSERT”
- 替代方案优先用
INSERT ... ON DUPLICATE KEY UPDATE,它内部由 InnoDB 原子处理,不会留下间隙锁等待窗口 - 如果必须分步做,至少把
SELECT改成普通读(不加锁),然后靠唯一索引约束捕获Duplicate entry错误来兜底,而不是依赖锁控制并发
间隙锁本身不是 bug,是 RR 隔离级别的必要代价;真正的问题在于,很多人以为“主键查询 = 安全”,却忽略了“查不到”这个分支的锁行为。实际线上出问题,八成卡在这儿。











