跳过导航

间隙锁为什么会导致线上死锁?从 Next-Key Lock 到排查实战

约 4 分钟...次浏览
专栏MySQL 与 ORM第 6 篇

很多死锁不是两个事务更新同一行,而是范围查询锁住“尚不存在的位置”,随后事务以不同顺序插入或更新。理解间隙锁,关键是把 SQL 映射为索引上的扫描区间。

三类锁

  • Record Lock:锁住索引记录。
  • Gap Lock:锁住两条索引记录之间的间隙,本身主要阻止插入。
  • Next-Key Lock:记录锁与其前方间隙的组合。

在 InnoDB 的 RR 隔离级别下,锁定读和写操作为防止幻影,通常对实际扫描到的索引范围使用 next-key/gap locking。完整唯一索引等值命中既有记录时通常只需记录锁;只匹配联合唯一索引的一部分、条件未命中、非唯一索引或范围扫描时,加锁区间更值得警惕。RC 会显著减少多数 gap locking,但外键检查、重复键检查等场景仍可能使用间隙相关锁。

可复现实验

CREATE TABLE booking(
 id BIGINT PRIMARY KEY AUTO_INCREMENT,
 room_id INT NOT NULL,
 booking_date DATE NOT NULL,
 UNIQUE KEY uk_room_date(room_id,booking_date)
) ENGINE=InnoDB;

两个事务都先检查日期无预约,再插入:

-- A
START TRANSACTION;
SELECT * FROM booking
WHERE room_id=1 AND booking_date='2026-08-01' FOR UPDATE;

-- B 执行同样 SELECT

-- A
INSERT INTO booking(room_id,booking_date) VALUES(1,'2026-08-01');

-- B
INSERT INTO booking(room_id,booking_date) VALUES(1,'2026-08-01');

在具体数据布局和版本下,两个事务可能都持有兼容的 gap lock,随后插入所需的插入意向锁相互等待,形成死锁;InnoDB 检测后回滚代价较小的一方。实验结果受已有记录影响,应在独立测试库观察实际锁:

SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
SHOW ENGINE INNODB STATUS\G

没有合适索引为何危险

SELECT * FROM booking WHERE booking_date='2026-08-01' FOR UPDATE;

若缺少以 booking_date 开头的索引,InnoDB 可能扫描并锁定大量索引范围。锁永远加在索引上;“只返回一行”不表示只锁一行,扫描路径才决定锁范围。

正确处理死锁

死锁是事务系统允许并发后可能出现的正常结果,应用必须能识别错误码 1213,只重试整个事务,并使用指数退避与随机抖动。不能只重试最后一条 SQL,因为原事务已经被回滚。

治理优先级:缩短事务;让不同代码路径以相同顺序访问资源;建立精准索引以缩小扫描;将“先查再插”改为唯一约束配合直接插入;必要时评估 RC,但降低隔离级别不是万能解法。

INSERT INTO booking(room_id,booking_date)
VALUES(1,'2026-08-01')
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id);

数据库唯一约束才是并发下防止重复预约的最终防线。上面的 LAST_INSERT_ID(id) 写法会修改当前连接的会话状态,连接池环境必须立即读取并保持调用边界清晰;如果并不需要返回既有 ID,更清晰的做法是捕获重复键错误并按幂等语义处理。应用层的“先查询不存在”存在检查与写入之间的竞态窗口。

排查证据链

从死锁日志提取每个事务持有与等待的锁、索引名、SQL 和事务持续时间,再还原访问顺序。保存完整日志,不要只截取最后一句 WE ROLL BACK TRANSACTION。可通过 innodb_print_all_deadlocks=ON 把所有死锁写入错误日志,但这会增加日志量并可能包含 SQL/业务数据,应配套脱敏、采集、保留周期和告警;临时排查后按运维规范恢复配置。

生产检查清单

  • 所有死锁是否保存双方 SQL、索引与事务上下文?
  • 不同业务路径是否按统一顺序锁定资源?
  • FOR UPDATE 的谓词是否有匹配索引?
  • “查后插”是否由唯一约束兜底?
  • 重试是否覆盖完整事务且有次数上限、退避和幂等?
  • 事务中是否包含 HTTP/RPC、文件 I/O 或大批量循环?

减少死锁的本质,是减少锁的数量和持有时间,并让锁的获取顺序可预测。

分享:
文章作者:狼码纪
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。文章可能参考了其他优秀文章,如有侵权请联系删除。