MySQL死锁排查与预防实战
2026/6/28 21:06:59 网站建设 项目流程

前言

线上日志里突然出现大量这个错误:

Deadlock found when trying to get lock; try restarting transaction

死锁是MySQL高并发场景下的常见问题。偶尔一两次可以通过业务重试解决,但如果频繁出现,就需要从根本上排查和优化。

这篇整理MySQL死锁的排查方法和预防策略。


一、查看死锁信息

MySQL有个命令能看到最近一次死锁的详情:

SHOWENGINEINNODBSTATUS\G

输出很长,找LATEST DETECTED DEADLOCK这部分:

*** (1) TRANSACTION: UPDATE orders SET status = 'paid' WHERE id = 1001 *** (1) HOLDS THE LOCK(S): -- 持有orders表的锁 *** (1) WAITING FOR THIS LOCK: -- 等inventory表的锁 *** (2) TRANSACTION: UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 2001 *** (2) HOLDS THE LOCK(S): -- 持有inventory表的锁 *** (2) WAITING FOR THIS LOCK: -- 等orders表的锁 *** WE ROLL BACK TRANSACTION (2)

经典的死锁场景:事务A锁了orders等inventory,事务B锁了inventory等orders,互相等。

二、分析死锁原因

知道是哪两个SQL了,回去翻代码。

原来下单逻辑里有两种调用顺序:

// 路径A:先改订单再扣库存updateOrderStatus(orderId,"paid");decreaseInventory(productId,1);// 路径B:先扣库存再改订单(另一个接口)decreaseInventory(productId,1);updateOrderStatus(orderId,"paid");

两个接口都在事务里,刚好并发了就死锁。

三、解决方案

最直接的办法:统一加锁顺序

不管哪个接口,都先操作orders再操作inventory(或者反过来,总之要一致)。

// 统一顺序:先orders后inventory@TransactionalpublicvoidprocessOrder(longorderId,longproductId){updateOrderStatus(orderId,"paid");// 永远先锁ordersdecreaseInventory(productId,1);// 再锁inventory}

如果涉及多条记录,按ID排序:

List<Long>ids=Arrays.asList(id1,id2,id3);Collections.sort(ids);for(Longid:ids){lockAndProcess(id);}

四、间隙锁导致的死锁

还有一种更诡异的死锁,两个事务操作的都不是同一行数据。

这通常是间隙锁的问题。RR隔离级别下,SELECT ... FOR UPDATE如果没命中数据,会锁一个"间隙"。

比如user_id有1、5、10三条记录:

-- 事务ASELECT*FROMordersWHEREuser_id=3FORUPDATE;-- 没有user_id=3的数据,但会锁住(1,5)这个间隙-- 事务BSELECT*FROMordersWHEREuser_id=7FORUPDATE;-- 锁住(5,10)这个间隙-- 然后两边各自INSERT-- 事务A想插入user_id=6,要等(5,10)的间隙锁-- 事务B想插入user_id=4,要等(1,5)的间隙锁-- 死锁

解决办法:

  1. 改用RC隔离级别(间隙锁少很多,但要注意幻读)
  2. 用唯一索引精确查询,避免范围锁
SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;

五、缩小事务范围

还有个常见问题是事务太长。事务越长,持有锁的时间越久,死锁概率越高。

// 这种写法不好@Transactionalpublicvoidprocess(){queryData();// 查数据callExternalApi();// 调外部接口,可能很慢updateDatabase();// 更新数据库}// 改成这样publicvoidprocess(){queryData();callExternalApi();// 外部调用放事务外面updateInTransaction();}@TransactionalpublicvoidupdateInTransaction(){updateDatabase();// 只有真正需要事务的操作}

六、监控与告警

建议加上监控:

# 简单脚本,每分钟检查死锁次数DEADLOCKS=$(mysql -e"SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks'"|awk'NR==2{print $2}')echo"$(date)deadlocks:$DEADLOCKS">>/var/log/deadlock.log

配合Prometheus的话:

-alert:MySQLDeadlockexpr:increase(mysql_global_status_innodb_deadlocks[5m])>0for:1m

死锁次数涨了就告警,别等业务反馈才知道。

七、业务层重试

有些场景死锁确实很难完全避免,那就在业务层做重试:

intretry=3;while(retry-->0){try{doTransaction();break;}catch(DeadlockExceptione){if(retry==0)throwe;Thread.sleep(100);// 等一下再试}}

MySQL检测到死锁会立即回滚一个事务,不会一直卡着,所以重试通常能成功。


总结

死锁本质是资源竞争问题,预防比解决更重要:

方法效果
统一加锁顺序最有效,从根本上避免死锁
缩小事务范围减少锁持有时间
合理使用索引减少锁的范围
降低隔离级别减少间隙锁(RC级别)
业务层重试兜底方案

记住两点:统一加锁顺序缩小事务范围,能解决大部分死锁问题。


有问题评论区交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询