【一次线上 MySQL 死锁问题的完整复盘与解析】

MySQL 死锁(Deadlock)问题看似简单,但背后涉及的原理却非常值得深挖。今天我们从现象、排查、复现到原理剖析,一步步带大家走一遍,希望能帮你在未来遇到类似问题时少走弯路。

1. 问题背景

我们的系统是一个典型的电商订单服务,核心表包括 orders(订单主表)和 order_items(订单明细表)。某天下午,监控系统突然报警:大量订单创建失败,错误日志中频繁出现:

sql 复制代码
Deadlock found when trying to get lock; try restarting transaction

乍一看是死锁,但奇怪的是,最近并没有上线新功能,也没有大促流量突增。那问题到底出在哪?

2. 初步排查

首先,我们立刻登录数据库,执行:

sql 复制代码
SHOW ENGINE INNODB STATUS\G

在输出的 LATEST DETECTED DEADLOCK 部分,找到了关键信息:

sql 复制代码
------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-12-17 21:32:05 0x7f8a1c0b2700
*** (1) TRANSACTION:
TRANSACTION 12345678, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1001, OS thread handle 123456, query id 987654 localhost user update
INSERT INTO order_items (order_id, product_id, quantity) VALUES (10001, 2001, 2)

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 123 page no 456 n bits 72 index PRIMARY of table `db`.`order_items` trx id 12345678 lock_mode X locks rec but not gap

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 456 n bits 72 index idx_order_id of table `db`.`order_items` trx id 12345678 lock_mode X locks gap before rec insert intention waiting

*** (2) TRANSACTION:
TRANSACTION 12345679, ACTIVE 0 sec inserting
...
INSERT INTO order_items (order_id, product_id, quantity) VALUES (10002, 2002, 1)
...
WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 456 n bits 72 index idx_order_id of table `db`.`order_items` trx id 12345679 lock_mode X locks gap before rec insert intention waiting

看起来两个事务都在往 order_items 表插入数据,并且都在等待对方释放对 idx_order_id 索引上的间隙锁(gap lock)。

3. 复现死锁

为了验证猜测,我们在测试环境构造了如下场景:

表结构简化如下:

sql 复制代码
CREATE TABLE `order_items` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_id` bigint NOT NULL,
  `product_id` bigint NOT NULL,
  `quantity` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB;

模拟两个并发事务:

事务 A:

sql 复制代码
BEGIN;
INSERT INTO order_items (order_id, product_id, quantity) VALUES (10001, 2001, 2);
-- 暂停几秒,模拟业务逻辑处理

事务 B:

sql 复制代码
BEGIN;
INSERT INTO order_items (order_id, product_id, quantity) VALUES (10002, 2002, 1);
-- 同样暂停

**结果:**当两个事务几乎同时执行 INSERT 时,其中一个会报死锁错误!

4. 原理分析:为什么 INSERT 也会死锁?

很多人以为只有 UPDATE/DELETE 才会加行锁,其实 INSERT 在某些情况下也会触发间隙锁(Gap Lock),尤其是在存在二级索引(如 idx_order_id)的情况下。

4.1 InnoDB 的插入意向锁(Insert Intention Lock)

当执行 INSERT 时,InnoDB 会先申请一个 插入意向锁(Insert Intention Lock),这是一种特殊的间隙锁,表示"我打算在这个区间插入一条记录"。

例如,如果当前 idx_order_id 中已有 order_id = [10000, 10005],那么插入 10001 和 10002 都会落在同一个间隙(10000, 10005)内。

4.2 间隙锁 + 插入意向锁 = 死锁温床

虽然插入意向锁之间 不会互相阻塞,但如果某个事务在插入前已经持有该间隙上的排他锁(比如因为之前有 DELETE 或 UPDATE 操作),那么后续的插入意向锁就会被阻塞。

但在我们这个案例中,并没有显式的 DELETE/UPDATE,为什么还会死锁?

**关键在于:**InnoDB 在 RR(Repeatable Read)隔离级别下,会对唯一索引冲突做特殊处理。

虽然 order_id 不是唯一索引,但当多个 INSERT 同时尝试在同一个间隙插入时,InnoDB 会为每个插入操作加一个 gap lock + insert intention lock 的组合。如果两个事务交叉请求这些锁,就可能形成环形等待------即死锁。

更具体地说:

事务 A 插入 10001,需要在 idx_order_id 上 (10000, 10005) 区间加插入意向锁。

事务 B 插入 10002,也需要在同一区间加插入意向锁。

但由于 InnoDB 的锁管理机制,在高并发下,这两个插入操作可能分别先获取了主键上的 X 锁,再尝试获取二级索引上的 gap 锁,而此时对方已持有部分资源,导致互相等待。

MySQL 的死锁检测器发现环形依赖后,会主动回滚其中一个事务,抛出 Deadlock 错误。

**注意:**这个问题在 READ COMMITTED 隔离级别下通常不会发生,因为 RC 不使用 gap lock(除了外键和唯一索引冲突检查)。

5. 解决方案

方案 1:重试机制(最常用)

由于死锁是瞬时状态,大多数应用框架(如 Spring)都支持事务自动重试。我们给订单创建接口加上了最多 3 次重试逻辑,问题大幅缓解。

sql 复制代码
@Retryable(value = {DeadlockLoserDataAccessException.class}, maxAttempts = 3, backoff = @Backoff(delay = 100))
public void createOrder(Order order) {
    // 业务逻辑
}

方案 2:降低隔离级别(谨慎使用)

将事务隔离级别从 RR 改为 RC,可以避免 gap lock,从而消除此类死锁。但需评估是否会影响业务一致性(比如幻读问题)。

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

方案 3:优化索引设计

如果 order_id 分布非常集中(比如都是连续递增),考虑是否真的需要 idx_order_id?或者能否用其他方式(如分区、批量插入)减少并发冲突。

方案 4:批量插入 + 顺序提交

将多个小事务合并为一个大事务,按 order_id 排序后批量插入,可显著减少锁竞争。但要注意事务过大带来的其他风险(如 binlog 膨胀、回滚成本高)。

6. 总结

  • 死锁不一定来自 UPDATE:INSERT 在二级索引上也可能引发死锁,尤其是在 RR 隔离级别下
  • SHOW ENGINE INNODB STATUS 是神器:务必学会看懂 deadlock 日志,它能告诉你谁在等谁、持有什么锁
  • 重试是最简单有效的兜底策略:对于偶发性死锁,自动重试比改架构更务实
  • 不要盲目去掉索引:索引虽可能引入锁竞争,但去掉后查询性能可能崩盘,需权衡

数据库的世界里,表面平静,底下暗流涌动。一个看似普通的 INSERT,背后可能是 InnoDB 锁机制、隔离级别、索引结构多重因素交织的结果。希望今天的复盘能帮你下次遇到死锁时,不再手足无措。

如果你觉得这篇文章有帮助,欢迎转发到你的技术群,也欢迎关注【数据库干货铺】,我会持续分享一线实战经验,不灌水,只讲干货。

相关推荐
风月歌2 小时前
基于微信小程序的学习资料销售平台源代码(源码+文档+数据库)
java·数据库·mysql·微信小程序·小程序·毕业设计·源码
qq2439201612 小时前
mysql导致的内存泄漏Abandoned connection cleanup thread
数据库·mysql
·云扬·2 小时前
深入理解MySQL InnoDB MVCC:原理、实验与实践
数据库·mysql
Macbethad2 小时前
数据库架构技术总结:MySQL主从/读写分离与PostgreSQL高可用
mysql·postgresql·数据库架构
无心水2 小时前
爆款实战!Vue3+Spring Boot+MySQL实现电商商品自动分类系统(含三级类目管理+规则兜底)
spring boot·mysql·分类·vue3商品分类·spring boot电商系统·三级类目管理·商品自动分类
IvorySQL2 小时前
版本发布| IvorySQL 5.1 发布
数据库·人工智能·postgresql·开源
yuniko-n2 小时前
【MySQL】通俗易懂的 MVCC 与事务
数据库·后端·sql·mysql
啦啦啦~~~7542 小时前
【最新版】Edge浏览器安装!绿色增强版+禁止Edge更新的软件+彻底卸载Edge软件
数据库·阿里云·电脑·.net·edge浏览器
程序边界2 小时前
金仓数据库助力Oracle迁移:一场国产数据库的逆袭之旅
数据库·oracle