InnoDB deadlock 日志中 “lock mode X locks rec but gap lock” 的含义

2次阅读

这是 next-key lock 的退化形式,即对记录加排他锁(X)且对其前隙加间隙锁,常见于非唯一索引范围更新/删除时为防幻读而自动施加的混合锁。

InnoDB deadlock 日志中 “lock mode X locks rec but gap lock” 的含义

“lock mode X locks rec but gap lock” 是什么锁?

这是 InnoDB 在死锁日志中对一种混合锁类型的简略描述,不是标准锁名,而是表示:当前事务持有的锁,既包含对记录本身(rec)的排他锁(X),又包含对该记录前一个间隙(gap)的间隙锁

它常见于 UPDATEdelete 语句在非唯一索引(或无索引)上进行范围条件匹配时,InnoDB 为避免幻读而自动加上的组合锁。注意:这里 “rec but gap” 并非“记录锁 + 间隙锁”的简单叠加,而是指该锁在行为上同时覆盖记录和其前隙 —— 实际是 next-key lock 的一种退化表现,但因扫描过程中遇到已存在记录,导致间隙部分被“截断”。

典型触发场景包括:

  • 执行 UPDATE t SET c=1 WHERE non_unique_idx > 10 AND non_unique_idx ,且索引值 15 已存在
  • 在可重复读(RR)隔离级别下,select ... for UPDATE 扫描非唯一二级索引范围
  • 插入前做唯一性检查失败回滚后残留的锁状态(较少见)

为什么死锁日志里不直接写 “next-key lock”?

因为 InnoDB 日志只反映锁的最终生效形态,而非加锁意图。next-key lock 是理论模型(记录锁 + 间隙锁),但在实际加锁过程中,若目标记录已存在,InnoDB 可能只在该记录上加 X 锁,并在其前隙加间隙锁 —— 此时记录锁与间隙锁作用域不连续,日志就拆开描述为 “locks rec but gap lock”。

关键区别在于:

  • lock mode X locks rec → 对具体聚集索引记录(或二级索引记录)加排他锁
  • lock mode X locks gap before rec → 单独间隙锁(不锁记录)
  • lock mode X locks rec but gap lock → 同一操作引发两种锁,且间隙落在该记录之前(即:封锁区间为 (prev_rec, current_rec])

这个锁会阻塞哪些操作?

它会同时阻塞两类动作:

对记录本身的修改/删除:

  • 其他事务尝试 UPDATEDELETE 同一条记录(聚集索引主键或二级索引项)→ 等待 X

对间隙的插入:

  • 其他事务尝试在该记录「之前」的间隙中插入新值(例如:当前记录索引值为 15,间隙为 (10, 15),则插入 12、14 都会被阻塞)→ 等待间隙锁

特别注意:它不阻塞在该记录「之后」插入(如插入 16),除非有另一个 next-key 锁覆盖 (15, next_rec]。

如何验证和排查这类锁?

最直接方式是结合 INFORMATION_SCHEMA.INNODB_TRXINNODB_LOCKSmysql 5.7)或 INNODB_LOCK_WaiTS(8.0+ 已移除,改用 performance_schema.data_locks)查看实时锁状态。

常用诊断步骤:

  • 从死锁日志中提取 TRANSACTION ID 和涉及的 tableINDEXHEAP NO
  • SELECT * FROM performance_schema.data_locks WHERE LOCK_TRX_ID = 'xxx';,确认 LOCK_MODE 是否含 X,REC_NOT_GAPX,GAP 组合
  • SELECT * FROM sys.innodb_lock_waits;(需启用 sys schema)快速定位阻塞链
  • 检查对应 SQL 是否用了非唯一索引做范围查询 —— 这是最常见根源

真正容易被忽略的是:这种锁在 RR 级别下不会因语句结束立即释放,而是持续到事务提交或回滚;如果应用使用长事务或未显式提交,它可能长期持锁并成为死锁温床。

text=ZqhQzanResources