订单接口偶发死锁,传统排查要逐行分析死锁日志和代码。本文记录如何将
SHOW ENGINE INNODB STATUS输出和事务代码交给 AI,快速理清锁冲突链路并给出修复方案,附完整排查流程和 AI 提示词模板。

故障现象:订单状态更新偶发失败
事情要从上周三说起。用户投诉"支付成功后订单状态还是待支付",而且不是每次都出现,大概每几百笔订单出现一次。我翻日志看到:
csharp
Deadlock found when trying to get lock; try restarting transaction
果然,死锁。但这个小规模系统怎么会有死锁?订单表只有几十万行,并发也不算高。传统的死锁排查流程是:SHOW ENGINE INNODB STATUS → 翻出最新死锁日志 → 逐行分析事务1在等什么锁、事务2在持有什么锁 → 根据 SQL 反推代码 → 定位冲突原因。这个过程,我之前做一次大概要花一个多小时。
这次我决定换个方式:把死锁日志和涉及到的相关代码,一起喂给 AI,让它来帮我做锁冲突分析。
第一步:拿到完整的死锁日志
在 MySQL 中执行:
sql
SHOW ENGINE INNODB STATUS\G
截取 LATEST DETECTED DEADLOCK 段落。我们的日志如下(脱敏处理):
text
------------------------
LATEST DETECTED DEADLOCK
------------------------
2026-05-25 14:22:33 0x7f8b4c001700
*** (1) TRANSACTION:
TRANSACTION 4215890334, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 218, query id 58320171 localhost root updating
UPDATE orders SET status='paid', payment_id='PX123456' WHERE id=188888
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 58 page no 412 n bits 72 index PRIMARY of table `shop`.`orders` trx id 4215890334 lock_mode X locks rec but not gap
Record lock, heap no 5 PHYSICAL RECORD: n_fields 12; compact format; info bits 0
0: len 4; hex 8002e208; asc ;; (id=188888)
...
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 420 n bits 72 index idx_user_id of table `shop`.`orders` trx id 4215890334 lock_mode X locks rec but not gap waiting
Record lock, heap no 8 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80001a4b; asc K;; (user_id=6731)
1: len 4; hex 8002e208; asc ;; (id=188888)
...
*** (2) TRANSACTION:
TRANSACTION 4215890335, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 219, query id 58320172 localhost root updating
UPDATE orders SET status='refunding', refund_id='RF987654' WHERE user_id=6731 AND status='paid'
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 58 page no 420 n bits 72 index idx_user_id of table `shop`.`orders` trx id 4215890335 lock_mode X locks rec but not gap
Record lock, heap no 8 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80001a4b; asc K;; (user_id=6731)
1: len 4; hex 8002e208; asc ;; (id=188888)
...
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 412 n bits 72 index PRIMARY of table `shop`.`orders` trx id 4215890335 lock_mode X locks rec but not gap waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 12; compact format; info bits 0
0: len 4; hex 8002e208; asc ;; (id=188888)
...
*** WE ROLL BACK TRANSACTION (2)
这个日志看着头大,但仔细解读:事务1在根据主键更新订单状态,它已经持有 id=188888 的 X 锁,却在等待 idx_user_id 上的一把锁;事务2正在按 user_id 更新这批订单,已经持有那个索引上的锁,却在等待主键 id=188888 的锁。典型互相等待。
但为什么会有两条 SQL 同时操作同一个订单?代码里到底怎么写的?
第二步:把相关代码喂给 AI,让它分析锁冲突
我找到 OrderService 里两处涉及更新订单状态的方法:
- 支付回调更新:根据订单 ID 更新状态。
- 批量退款处理 :根据用户 ID 查出所有
paid状态的订单,逐个发起退款,同时更新状态。
代码简版:
java
// 支付回调处理
@Transactional
public void handlePaymentCallback(String orderId, String paymentId) {
Orders order = ordersMapper.selectById(orderId);
if (order != null && order.getStatus().equals("pending")) {
ordersMapper.updateStatus(orderId, "paid", paymentId);
}
}
// 批量退款(运营工具)
@Transactional
public void batchRefundByUser(Long userId) {
List<Orders> paidOrders = ordersMapper.selectByUserIdAndStatus(userId, "paid");
for (Orders order : paidOrders) {
refundService.createRefund(order.getId());
ordersMapper.updateStatus(order.getId(), "refunding");
}
}
我把这两段代码和死锁日志一起发给 GPT-5.5,提示词:
text
你是一位资深 MySQL DBA 和 Java 后端专家。下面是一份 InnoDB 死锁日志,以及相关的 Java 代码。
## 死锁日志
[粘贴上述死锁日志]
## 相关 Java 代码
[粘贴 handlePaymentCallback 和 batchRefundByUser 代码]
请分析:
1. 这两个事务为什么会产生死锁?用通俗的语言描述锁的获取顺序和冲突点。
2. 指出代码中哪些地方导致了锁顺序不一致。
3. 给出至少两种修复方案,包含具体代码修改。
4. 建议如何通过加索引或调整事务隔离级别来避免类似问题。
AI 在几秒内给出了详细分析,核心结论让我豁然开朗:
死锁原因:
- 事务1(支付回调)先通过主键索引锁定 id=188888 的行,然后因为某种原因需要锁定
idx_user_id上的记录(可能是间隙锁或二级索引更新)。- 事务2(批量退款)先通过
idx_user_id锁定 user_id=6731 的所有订单记录(包括 id=188888),然后再去主键索引锁定具体行。- 两个事务以相反的顺序获取锁,形成死锁。
根本原因 :事务2 的
SELECT ... WHERE user_id = ? AND status = 'paid'在 InnoDB 下会对扫描到的行加锁,即使最终没更新的行也会被锁(如果是 RC 隔离级别会好一些,但 gap lock 仍可能存在)。而事务1 在更新主键后,如果status字段上有索引或者 update 语句影响了二级索引,也会去获取idx_user_id的锁。锁的顺序不一致,并发时就会死锁。
AI 还指出了代码中一个致命细节:batchRefundByUser 在循环里逐条更新,并且整个方法加了 @Transactional,导致事务持有锁的时间非常长,大大增加了死锁概率。
第三步:根据 AI 的建议修复代码
AI 给了两个修复方案,我选择了更稳妥的一种:统一锁的获取顺序 + 缩短事务时间。
修复1:调整批量退款的逻辑,先排序再处理
确保所有事务都先锁主键,再锁二级索引,打破循环等待。
java
@Transactional
public void batchRefundByUser(Long userId) {
List<Orders> paidOrders = ordersMapper.selectByUserIdAndStatus(userId, "paid");
// 关键:按主键 ID 排序,确保所有事务以相同的顺序访问行
paidOrders.sort(Comparator.comparingLong(Orders::getId));
for (Orders order : paidOrders) {
// 逐条处理,但可以每次只锁定必要的行
processRefund(order.getId());
}
}
// 拆出独立事务,缩小锁范围
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processRefund(Long orderId) {
Orders order = ordersMapper.selectById(orderId);
if (order != null && order.getStatus().equals("paid")) {
refundService.createRefund(orderId);
ordersMapper.updateStatus(orderId, "refunding");
}
}
这里用了 REQUIRES_NEW,将每条订单的处理独立成一个事务,锁的持有时间从原来遍历整个列表缩短到单条记录,大大降低了死锁概率。
修复2:支付回调也做防御性查询
java
@Transactional
public void handlePaymentCallback(String orderId, String paymentId) {
// 改为用 select ... for update 锁定行,确保一致性
Orders order = ordersMapper.selectByIdForUpdate(orderId);
if (order != null && order.getStatus().equals("pending")) {
ordersMapper.updateStatus(orderId, "paid", paymentId);
}
}
用 SELECT ... FOR UPDATE 显式加锁,避免在 update 时隐式锁升级引发意外的锁等待。
修复3:添加复合索引减少锁范围
AI 建议在 orders 表上创建 idx_user_status (user_id, status),这样批量退款查询时扫描的行更少,锁定的行也更少。
sql
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
加上后,SELECT ... WHERE user_id=6731 AND status='paid' 只锁定符合条件的行,而不是扫描全部后加锁(取决于隔离级别,但索引过滤能显著减少锁)。
第四步:验证修复效果
修复上线后,观察了一周,死锁从之前每天出现 3-5 次降为零。
我们用 SHOW ENGINE INNODB STATUS 和死锁监控面板(之前搭的 Prometheus 监控)确认再无告警。
为什么 AI 能这么快定位死锁?
死锁日志本质上是结构化的锁等待图,但人工解读需要同时理解 InnoDB 的锁机制和业务代码的 SQL 执行顺序。AI 恰好擅长:
- 解析日志中的事务持有锁和等待锁的关系。
- 将日志中的索引名、锁类型与代码中的 SQL 语句匹配。
- 推断出锁获取顺序,指出死锁循环。
人工可能需要反复查阅文档和代码,但 AI 一次就能关联起来。
可复用的死锁排查 + AI 辅助流程
下次遇到死锁,直接套这个模板:
- 获取死锁日志:
SHOW ENGINE INNODB STATUS\G中的LATEST DETECTED DEADLOCK部分。 - 定位涉及的表和 SQL:从日志中找到被锁的表和正在执行的语句。
- 找到对应代码:根据 SQL 反查代码,把相关方法(带上事务注解)全部收集。
- 组装提示词发给 AI:
text
你是数据库和 Java 专家。请分析以下死锁日志和 Java 代码。
## 死锁日志
[粘贴]
## 代码
[粘贴涉及事务的方法]
## 要求
1. 描述死锁发生的锁顺序。
2. 指出代码中导致死锁的具体原因。
3. 提供修复方案,包括 SQL 和 Java 代码。
4. 建议是否需要加索引。
- 根据 AI 回答,选择最合适的方案修改,验证。
整个流程从收集信息到得到修复方案,10 分钟左右,而以往手动排查可能要 1-2 小时。
总结:
- 死锁的根源往往是锁顺序不一致,而不一定是高并发。我们系统并发并不高,但两个事务以相反顺序获取锁,照样死锁。
- 长事务是死锁的温床,把一个大事务拆成多个小事务,不仅提高并发,也减少死锁。
- AI 是分析日志和代码关系的利器,特别是 InnoDB 死锁日志这种对人不友好但对 AI 结构化程度高的数据。
- 建对索引能减少锁范围,间接避免死锁,不要只把索引当成查询优化工具。