记一次 MySQL 8.0 死锁排查:间隙锁引发的线上事故

Debugging a MySQL 8.0 Deadlock: A Production Incident Caused by Gap Locks

| David Wang | 2026-07-26T14:34:03

线上突然告警大量死锁,排查了一整天才发现是间隙锁在特定场景下的诡异行为。这篇文章完整还原了排查过程。

A complete walkthrough of debugging a production deadlock incident caused by gap lock behavior in MySQL 8.0.

## 事故现场 周三下午三点,监控突然炸了。告警群里刷刷刷地弹消息:数据库死锁,QPS 暴跌。 赶紧看了一下 Grafana,死锁频率从 0 飙到每分钟 200+。打开慢查询日志,全是 `LATEST DETECTED DEADLOCK`。 ## 第一步:看死锁日志 ```sql SHOW ENGINE INNODB STATUS\G ``` 死锁信息大概是这样的(简化版): ``` *** (1) TRANSACTION: TRANSACTION 12345, ACTIVE 0 sec inserting INSERT INTO order_item (order_id, product_id, quantity) VALUES (1001, 5, 2) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS gap before rec *** (2) TRANSACTION: TRANSACTION 12346, ACTIVE 0 sec inserting INSERT INTO order_item (order_id, product_id, quantity) VALUES (1002, 5, 3) *** (2) HOLDS THE LOCK(S): RECORD LOCKS gap before rec ``` 两个事务都在 insert,都在等间隙锁。嗯?Insert 怎么会有间隙锁? ## 第二步:还原场景 经过代码排查,发现业务逻辑是这样的: ```sql -- 事务开始 BEGIN; -- 先查一下有没有重复记录(用了 SELECT ... FOR UPDATE) SELECT * FROM order_item WHERE order_id = ? AND product_id = ? FOR UPDATE; -- 没有就插入 INSERT INTO order_item (order_id, product_id, quantity) VALUES (?, ?, ?); COMMIT; ``` 问题出在 `SELECT ... FOR UPDATE` 上。当查询结果为空时,InnoDB 不会加行锁(因为没有行),而是会加一个**间隙锁(Gap Lock)**,锁住这个"空隙"。 ## 间隙锁的诡异之处 间隙锁之间是**不互斥**的!两个事务可以同时持有同一个间隙的间隙锁。但是,当它们都试图在这个间隙里 INSERT 时,就需要**插入意向锁(Insert Intention Lock)**,而插入意向锁和间隙锁是冲突的。 于是就出现了经典的死锁: 1. 事务 A 拿到间隙锁 2. 事务 B 拿到间隙锁(间隙锁不互斥,可以同时拿) 3. 事务 A 要插入 → 需要插入意向锁 → 被事务 B 的间隙锁阻塞 4. 事务 B 要插入 → 需要插入意向锁 → 被事务 A 的间隙锁阻塞 5. 死锁! ## 解决方案 想了几个方案: **方案一:改隔离级别为 READ COMMITTED** RC 级别下不会有间隙锁,但会引入幻读问题,而且改全局隔离级别影响太大,pass。 **方案二:用 INSERT ... ON DUPLICATE KEY UPDATE** ```sql INSERT INTO order_item (order_id, product_id, quantity) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity); ``` 不需要先 SELECT 了,直接 INSERT,靠唯一索引去重。这个方案最终被采用了。 **方案三:应用层分布式锁** 用 Redis 锁在应用层做互斥,但增加了复杂度,而且锁的粒度不好控制。 ## 修复上线 改完代码,灰度发布了一批机器,死锁立刻消失。然后全量发布,问题彻底解决。 ## 复盘 这个问题的根因是**对间隙锁的理解不够深入**。很多人知道 `SELECT ... FOR UPDATE` 会加锁,但不知道查不到数据时加的是间隙锁而不是行锁。更不知道间隙锁之间不互斥但和插入意向锁互斥这个细节。 建议大家把《MySQL 技术内幕:InnoDB 存储引擎》的锁章节好好读一遍,能避免很多坑。


## The Incident On a Wednesday afternoon, monitoring alerts exploded with database deadlocks. Deadlock frequency spiked from 0 to 200+ per minute. ## Root Cause Analysis The issue was traced to a common pattern: `SELECT ... FOR UPDATE` followed by `INSERT`. When the SELECT returns no rows, InnoDB places a gap lock instead of a row lock. Gap locks don't conflict with each other, but both conflict with insert intention locks, creating a classic deadlock scenario. ## The Fix Replaced the SELECT-then-INSERT pattern with `INSERT ... ON DUPLICATE KEY UPDATE`, eliminating the need for the gap lock entirely. Deadlocks disappeared immediately after deployment. ## Key Takeaway Understanding InnoDB's gap lock behavior is critical. Gap locks are non-exclusive with each other but conflict with insert intention locks - a subtle detail that can cause severe production issues.

← Back to News