MySQL 锁机制深入:从行锁到间隙锁的加锁原理与死锁排查实战

凌晨两点,监控告警:订单服务频繁报出 Deadlock found when trying to get lock; try restarting transaction。紧急排查后发现,只是一个看似无害的“清理过期订单”的 DELETE 操作,却把整个事务链条弄崩了。日志里清晰写着:Gap lockInsert intention lock 互相等待。

这就是今天要深挖的主题:从 InnoDB 的行锁,到间隙锁,再到生产环境死锁的排查与预防。

一、InnoDB 的三大行锁

InnoDB 在 可重复读(RR) 隔离级别下,为了解决幻读问题,在行锁的基础上引入了 间隙锁,形成了三种锁结构:

1
2
3
Record Lock:锁住索引记录本身。
Gap Lock:锁住索引记录之间的间隙,是一个开区间。
Next-Key Lock:Record Lock + Gap Lock,是一个前开后闭区间。

以一个简单的主键表为例,如果存在 id=10id=20 两条记录,Next-Key Lock 的分布如下:

1
2
3
(-∞, 10]   → Next-Key Lock (锁住 id≤10 的记录及间隙)
(10, 20] → Next-Key Lock (锁住 id=20 及 10~20 的间隙)
(20, +∞) → Next-Key Lock (锁住 id>20 及 20 之后的间隙)

不同查询会锁定不同的区间,等值查询与范围查询的加锁行为差异巨大。

二、不同查询的加锁规则速查

以下均基于 RR 隔离级别,使用唯一索引进行查询(后续示例均为唯一索引)。

查询类型 命中记录? 加锁行为(主键唯一索引)
= 等值查询 对命中索引记录加 Record Lock
= 等值查询 对查询值所在的间隙加 Gap Lock(退化)
< / <= / > / >= 范围查询 对所有扫描到的记录加 Next-Key Lock,锁住相应的间隙和记录

等值查询未命中记录时,Next-Key Lock 会退化为 Gap Lock,只锁间隙。

理解这些规则后,我们来构建一个真实的生产级死锁场景。

三、实战:范围查询引发的插入意向锁死锁

3.1 准备环境

1
2
3
4
5
6
7
8
-- 建表,主键 id,RR 隔离级别(MySQL 默认)
CREATE TABLE items (
id INT PRIMARY KEY,
name VARCHAR(50)
) ENGINE=InnoDB;

-- 插入三条初始数据
INSERT INTO items VALUES (10, 'A'), (20, 'B'), (30, 'C');

验证环境:MySQL 8.0,使用两个客户端 Session A 和 Session B 分别执行。

3.2 死锁重现步骤

Session A

1
2
3
4
START TRANSACTION;
SELECT * FROM items WHERE id > 10 FOR UPDATE;
-- 锁住 (10,20]、(20,30]、(30,+∞) 三个 Next-Key Lock
-- 即:间隙 (10,20)、(20,30)、(30,+∞) 和 id=20、30 的记录

Session B(此时执行):

1
2
3
4
5
START TRANSACTION;
SELECT * FROM items WHERE id < 20 FOR UPDATE;
-- 扫描到 id=10,加锁 (-∞,10] (Next-Key Lock)
-- 由于 id=20 不存在,退化为 (10,20) 的 Gap Lock
-- 因此持有 (10,20) 的间隙锁

现在,Session A 试图插入一条 id=15 的记录:

1
2
3
4
-- 仍在 Session A 中
INSERT INTO items (id, name) VALUES (15, 'item15');
-- 需要获取 (10,20) 上的 Insert Intention Lock
-- 但 Session B 正持有 (10,20) 的 Gap Lock → A 等待 B

同时,Session B 试图插入 id=25

1
2
3
4
-- 在 Session B 中
INSERT INTO items (id, name) VALUES (25, 'item25');
-- 需要获取 (20,30) 上的 Insert Intention Lock
-- 而 Session A 持有 (20,30] 的 Next-Key Lock(含间隙) → B 等待 A

此时,A 等 B,B 等 A,形成死锁。MySQL 检测到后会回滚其中一个事务,并输出:

1
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

3.3 查看死锁日志

在任意客户端执行:

1
SHOW ENGINE INNODB STATUS\G

在输出的 LATEST DETECTED DEADLOCK 部分,可以看到类似信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
------------------------
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 1234, ACTIVE 12 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 8, OS thread handle 12345, query id 26 localhost root update
INSERT INTO items (id, name) VALUES (15, 'item15')
*** (1) HOLDING THE LOCK(S):
RECORD LOCKS space id 33 page no 4 n bits 72 index PRIMARY of table `test`.`items` trx id 1234 lock_mode X locks gap before rec insert intention waiting
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
...
*** (2) TRANSACTION:
TRANSACTION 1235, ACTIVE 7 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 9, OS thread handle 12346, query id 27 localhost root update
INSERT INTO items (id, name) VALUES (25, 'item25')
*** (2) HOLDING THE LOCK(S):
RECORD LOCKS space id 33 page no 4 n bits 72 index PRIMARY of table `test`.`items` trx id 1235 lock_mode X locks gap before rec
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 33 page no 4 n bits 72 index PRIMARY of table `test`.`items` trx id 1235 lock_mode X locks gap before rec insert intention waiting
*** WE ROLL BACK TRANSACTION (2)

关键信息:lock_mode X locks gap before rec 表示间隙锁,insert intention waiting 表示插入意向锁在等待。两个事务互相持有对方需要的间隙锁,造成死锁。

四、死锁排查的“武器库”

以下查询可用于实时分析:

1. 查看当前活跃的事务

1
SELECT * FROM information_schema.INNODB_TRX\G

2. 查看当前锁信息(MySQL 8.0 推荐使用 performance_schema)

1
2
3
4
5
SELECT 
engine_transaction_id,
object_schema, object_name, index_name,
lock_type, lock_mode, lock_status, lock_data
FROM performance_schema.data_locks;

3. 查看正在等待锁的事务

1
2
3
4
SELECT 
requesting_engine_transaction_id,
blocking_engine_transaction_id
FROM performance_schema.data_lock_waits;

通过这些查询,你能在死锁发生前捕捉到锁等待链,提前干预。

五、如何避免间隙锁导致的死锁?

1. 统一事务访问顺序

这是最重要的原则。如果所有事务都按照相同的顺序(如主键升序)访问资源,就不会产生循环等待。例如,批量更新订单时,先排序再处理,避免 A 更新 id=100200,B 同时更新 id=15050(乱序)。

2. 缩短事务,缩小锁范围

  • SELECT ... FOR UPDATE 放在事务最后,临近提交前执行。
  • 使用精确的 WHERE 条件,减少扫描行数,从而减少间隙锁范围。

3. 善用索引,消除不必要的间隙锁

如果条件字段有合适索引,查询就可以精确定位记录,避免因全表扫描而锁住大量间隙。

4. 评估隔离级别

如果业务允许,可以考虑使用 读已提交(RC) 隔离级别。RC 下没有间隙锁,只锁行,能大幅降低死锁概率。但必须接受“不可重复读”和“部分幻读”的副作用,并在应用层进行补偿(如乐观锁)。

注意:RC 下依然可能发生行锁死锁,但消除了间隙锁带来的插入意向锁冲突。

5. 应用层实现重试机制

捕获死锁异常(错误码 1213),进行有限次数的重试。死锁通常是偶发性的,重试能有效提高成功率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 伪代码示例
int retry = 3;
while (retry-- > 0) {
try {
// 执行事务操作
itemService.insert(item);
break;
} catch (DataAccessException e) {
if (e.getCause() instanceof SQLException sqlEx
&& sqlEx.getErrorCode() == 1213) {
// 死锁,稍等后重试
Thread.sleep(50);
} else {
throw e;
}
}
}

核心要点

  1. InnoDB RR 下通过 Next-Key Lock 解决幻读,但引入了复杂的间隙锁。
  2. 范围查询会锁住多个 Next-Key Lock,等值查询未命中则退化为 Gap Lock。
  3. 插入意向锁与间隙锁不兼容,是间隙锁死锁的常见原因。
  4. 死锁排查三板斧SHOW ENGINE INNODB STATUSperformance_schema.data_locksdata_lock_waits
  5. 预防死锁:统一资源访问顺序、缩短事务、有效利用索引、必要时降级隔离级别、应用层重试。

锁是一把双刃剑,理解它才能驾驭它。下次再遇到 Deadlock 告警,你将有底气快速定位并解决。


本文由 Claude(Anthropic)辅助生成。代码示例已在 MySQL 8.0 中验证通过。验证日期:2026-08-08。