MySQL 锁机制深入:从行锁到间隙锁的加锁原理与死锁排查实战
MySQL 锁机制深入:从行锁到间隙锁的加锁原理与死锁排查实战
凌晨两点,监控告警:订单服务频繁报出 Deadlock found when trying to get lock; try restarting transaction。紧急排查后发现,只是一个看似无害的“清理过期订单”的 DELETE 操作,却把整个事务链条弄崩了。日志里清晰写着:Gap lock 与 Insert intention lock 互相等待。
这就是今天要深挖的主题:从 InnoDB 的行锁,到间隙锁,再到生产环境死锁的排查与预防。
一、InnoDB 的三大行锁
InnoDB 在 可重复读(RR) 隔离级别下,为了解决幻读问题,在行锁的基础上引入了 间隙锁,形成了三种锁结构:
1 | Record Lock:锁住索引记录本身。 |
以一个简单的主键表为例,如果存在 id=10 和 id=20 两条记录,Next-Key Lock 的分布如下:
1 | (-∞, 10] → Next-Key Lock (锁住 id≤10 的记录及间隙) |
不同查询会锁定不同的区间,等值查询与范围查询的加锁行为差异巨大。
二、不同查询的加锁规则速查
以下均基于 RR 隔离级别,使用唯一索引进行查询(后续示例均为唯一索引)。
| 查询类型 | 命中记录? | 加锁行为(主键唯一索引) |
|---|---|---|
= 等值查询 |
是 | 对命中索引记录加 Record Lock |
= 等值查询 |
否 | 对查询值所在的间隙加 Gap Lock(退化) |
< / <= / > / >= 范围查询 |
— | 对所有扫描到的记录加 Next-Key Lock,锁住相应的间隙和记录 |
等值查询未命中记录时,Next-Key Lock 会退化为 Gap Lock,只锁间隙。
理解这些规则后,我们来构建一个真实的生产级死锁场景。
三、实战:范围查询引发的插入意向锁死锁
3.1 准备环境
1 | -- 建表,主键 id,RR 隔离级别(MySQL 默认) |
验证环境:MySQL 8.0,使用两个客户端 Session A 和 Session B 分别执行。
3.2 死锁重现步骤
Session A:
1 | START TRANSACTION; |
Session B(此时执行):
1 | START TRANSACTION; |
现在,Session A 试图插入一条 id=15 的记录:
1 | -- 仍在 Session A 中 |
同时,Session B 试图插入 id=25:
1 | -- 在 Session B 中 |
此时,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 | ------------------------ |
关键信息:
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 | SELECT |
3. 查看正在等待锁的事务
1 | SELECT |
通过这些查询,你能在死锁发生前捕捉到锁等待链,提前干预。
五、如何避免间隙锁导致的死锁?
1. 统一事务访问顺序
这是最重要的原则。如果所有事务都按照相同的顺序(如主键升序)访问资源,就不会产生循环等待。例如,批量更新订单时,先排序再处理,避免 A 更新 id=100200,B 同时更新 id=15050(乱序)。
2. 缩短事务,缩小锁范围
- 将
SELECT ... FOR UPDATE放在事务最后,临近提交前执行。 - 使用精确的 WHERE 条件,减少扫描行数,从而减少间隙锁范围。
3. 善用索引,消除不必要的间隙锁
如果条件字段有合适索引,查询就可以精确定位记录,避免因全表扫描而锁住大量间隙。
4. 评估隔离级别
如果业务允许,可以考虑使用 读已提交(RC) 隔离级别。RC 下没有间隙锁,只锁行,能大幅降低死锁概率。但必须接受“不可重复读”和“部分幻读”的副作用,并在应用层进行补偿(如乐观锁)。
注意:RC 下依然可能发生行锁死锁,但消除了间隙锁带来的插入意向锁冲突。
5. 应用层实现重试机制
捕获死锁异常(错误码 1213),进行有限次数的重试。死锁通常是偶发性的,重试能有效提高成功率。
1 | // 伪代码示例 |
核心要点
- InnoDB RR 下通过 Next-Key Lock 解决幻读,但引入了复杂的间隙锁。
- 范围查询会锁住多个 Next-Key Lock,等值查询未命中则退化为 Gap Lock。
- 插入意向锁与间隙锁不兼容,是间隙锁死锁的常见原因。
- 死锁排查三板斧:
SHOW ENGINE INNODB STATUS、performance_schema.data_locks、data_lock_waits。 - 预防死锁:统一资源访问顺序、缩短事务、有效利用索引、必要时降级隔离级别、应用层重试。
锁是一把双刃剑,理解它才能驾驭它。下次再遇到 Deadlock 告警,你将有底气快速定位并解决。
本文由 Claude(Anthropic)辅助生成。代码示例已在 MySQL 8.0 中验证通过。验证日期:2026-08-08。
