死锁不是 MySQL 的异常行为,而是并发系统的正常风险。只要多个事务以不同顺序持有资源并等待对方,就可能死锁。InnoDB 会自动检测死锁,并回滚其中一个事务。业务系统需要做的是降低死锁概率,并在发生死锁时能快速定位和恢复。
供应链系统里,入库上架、库存转移、订单扣减都容易出现死锁,因为这些流程通常会同时更新库存主表、库存流水表、批次表和单据状态。
死锁排查流程
入库上架死锁案例
入库上架时,系统要把待上架数量转成可用库存:
1 | UPDATE scm_inventory |
另一个任务可能先更新入库明细,再更新库存:
1 | UPDATE scm_receipt_detail |
事务 A 持有库存记录,等待入库明细。事务 B 持有入库明细,等待库存记录。两边互相等待,就形成死锁。
死锁的四个条件
死锁通常满足四个条件:
- 互斥:资源一次只能被一个事务持有。
- 持有并等待:事务持有一个锁,同时等待另一个锁。
- 不可抢占:锁只能由持有者提交或回滚后释放。
- 循环等待:多个事务形成等待环。
业务上最容易控制的是循环等待。只要固定加锁顺序,就能显著降低死锁概率。
固定加锁顺序
对库存转移来说,要从 A 库位转到 B 库位。两个事务方向相反时容易死锁:
1 | 事务 1:锁 A,再锁 B |
解决办法是按稳定规则排序,比如按库存记录 ID 升序加锁:
1 |
|
SQL:
1 | SELECT id |
即使业务方向不同,底层锁顺序一致,死锁概率会低很多。
缩短事务范围
错误做法:
1 |
|
这段代码在事务里做远程调用。远程调用期间数据库锁一直不释放,任何并发库存操作都可能等待。
改进方式是把远程调用放在事务外,事务里只做核心数据变更:
1 | public void putaway(PutawayCommand command) { |
如何排查锁等待
第一步看当前连接:
1 | SHOW PROCESSLIST; |
第二步看 InnoDB 状态:
1 | SHOW ENGINE INNODB STATUS; |
重点找:
LATEST DETECTED DEADLOCK- 等待的锁类型。
- 等待的索引。
- 涉及的 SQL。
- 哪个事务被回滚。
MySQL 8 可以查询:
1 | SELECT * |
结合事务表可以定位阻塞链:
1 | SELECT r.trx_id waiting_trx, |
不同版本系统表略有差异,核心思路是找到等待者、阻塞者和对应 SQL。
业务层重试
死锁发生时,InnoDB 会回滚一个事务。对于库存扣减、上架、状态流转这类短事务,可以做有限重试:
1 | public void putawayWithRetry(PutawayCommand command) { |
重试必须满足幂等。比如上架流水要有业务唯一键,避免第一次事务部分成功但应用误判后重复写入。通常建议给业务流水加唯一键:
1 | UNIQUE KEY uk_biz_event (biz_type, biz_id) |
小结
死锁排查不能只看异常栈,要回到事务里的 SQL 顺序、索引和持锁时间。供应链系统降低死锁的关键措施是:固定加锁顺序,缩短事务范围,避免事务内远程调用,保证 SQL 命中索引,对可重试短事务做有限幂等重试。