那天凌晨2点15分,我正在家里睡得正香,突然被一阵急促的电话铃声惊醒。运维同事告诉我订单系统出现了大量死锁告警,用户提交的批量订单有近30%失败。我立刻打开电脑连上生产环境,在错误日志中看到了熟悉的提示:"Deadlock found when trying to get lock; try restarting transaction"。
这个电商平台的订单系统有个典型的高并发场景:每天凌晨会有大量商户批量更新订单状态。当时正好赶上促销活动,每秒并发量达到平时5倍。通过show engine innodb status命令查看最新死锁日志,发现两个事务在互相等待对方持有的锁:
code复制*** (1) TRANSACTION:
TRANSACTION 182335678, ACTIVE 0 sec updating rows
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s)
UPDATE orders SET status = 'shipped' WHERE merchant_id = 10086 AND create_time > '2023-06-01'
*** (2) TRANSACTION:
TRANSACTION 182335679, ACTIVE 0 sec updating rows
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 3 row lock(s)
UPDATE orders SET tracking_no = 'SF123456' WHERE order_id IN (SELECT order_id FROM orders WHERE merchant_id = 10086 LIMIT 100)
很多人以为行锁就是直接锁住磁盘上的数据行,其实不然。InnoDB的行锁是通过锁索引实现的,这个认知非常重要。我们的订单表上有三个索引:
当执行UPDATE orders SET status = 'shipped' WHERE merchant_id = 10086时:
这次事故完美符合死锁产生的所有条件:
遇到死锁不要慌,MySQL提供了完善的分析工具。我常用的排查步骤:
sql复制-- 查看当前所有事务
SELECT * FROM information_schema.INNODB_TRX;
-- 查看锁等待关系
SELECT * FROM performance_schema.events_waits_current;
-- 查看最近发生的死锁详情
SHOW ENGINE INNODB STATUS\G
重点关注LATEST DETECTED DEADLOCK段,它会显示:
通过EXPLAIN分析那两个冲突SQL的执行计划,发现了关键问题:
sql复制EXPLAIN UPDATE orders SET status = 'shipped' WHERE merchant_id = 10086;
结果显示使用了merchant_id索引,但需要回表查询。而批量更新订单号的那个SQL虽然用了主键,但子查询里又用到了merchant_id条件。这就形成了经典的"交叉加锁"场景。
经过分析,我们对订单表做了以下优化:
优化后的SQL执行时不再需要回表,直接通过覆盖索引完成更新:
sql复制ALTER TABLE orders
DROP INDEX idx_merchant_time,
ADD INDEX idx_merchant_time_status (merchant_id, create_time, status);
对于必须使用多语句事务的场景,我们制定了新的开发规范:
java复制// 伪代码示例
int retries = 3;
while(retries-- > 0){
try {
executeTransaction();
break;
} catch(DeadlockException e){
Thread.sleep(100 * (3 - retries)); // 指数退避
}
}
经过这次事故,我们总结了以下经验:
NOW()这次死锁排查经历让我深刻体会到,数据库优化不是一劳永逸的工作。随着业务量增长,需要持续监控和调整。现在我们的订单系统已经稳定运行了半年多,再没出现过类似问题。关键是要建立完善的预防机制,而不是等问题发生了再去救火。