MySQL死锁排查与预防实战

📅 发布时间:2026/7/3 23:00:25 👁️ 浏览次数:
MySQL死锁排查与预防实战
前言线上日志里突然出现大量这个错误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后inventoryTransactionalpublicvoidprocessOrder(longorderId,longproductId){updateOrderStatus(orderId,paid);// 永远先锁ordersdecreaseInventory(productId,1);// 再锁inventory}如果涉及多条记录按ID排序ListLongidsArrays.asList(id1,id2,id3);Collections.sort(ids);for(Longid:ids){lockAndProcess(id);}四、间隙锁导致的死锁还有一种更诡异的死锁两个事务操作的都不是同一行数据。这通常是间隙锁的问题。RR隔离级别下SELECT ... FOR UPDATE如果没命中数据会锁一个间隙。比如user_id有1、5、10三条记录-- 事务ASELECT*FROMordersWHEREuser_id3FORUPDATE;-- 没有user_id3的数据但会锁住(1,5)这个间隙-- 事务BSELECT*FROMordersWHEREuser_id7FORUPDATE;-- 锁住(5,10)这个间隙-- 然后两边各自INSERT-- 事务A想插入user_id6要等(5,10)的间隙锁-- 事务B想插入user_id4要等(1,5)的间隙锁-- 死锁解决办法改用RC隔离级别间隙锁少很多但要注意幻读用唯一索引精确查询避免范围锁SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;五、缩小事务范围还有个常见问题是事务太长。事务越长持有锁的时间越久死锁概率越高。// 这种写法不好Transactionalpublicvoidprocess(){queryData();// 查数据callExternalApi();// 调外部接口可能很慢updateDatabase();// 更新数据库}// 改成这样publicvoidprocess(){queryData();callExternalApi();// 外部调用放事务外面updateInTransaction();}TransactionalpublicvoidupdateInTransaction(){updateDatabase();// 只有真正需要事务的操作}六、监控与告警建议加上监控# 简单脚本每分钟检查死锁次数DEADLOCKS$(mysql -eSHOW GLOBAL STATUS LIKE Innodb_deadlocks|awkNR2{print $2})echo$(date)deadlocks:$DEADLOCKS/var/log/deadlock.log配合Prometheus的话-alert:MySQLDeadlockexpr:increase(mysql_global_status_innodb_deadlocks[5m])0for:1m死锁次数涨了就告警别等业务反馈才知道。七、业务层重试有些场景死锁确实很难完全避免那就在业务层做重试intretry3;while(retry--0){try{doTransaction();break;}catch(DeadlockExceptione){if(retry0)throwe;Thread.sleep(100);// 等一下再试}}MySQL检测到死锁会立即回滚一个事务不会一直卡着所以重试通常能成功。总结死锁本质是资源竞争问题预防比解决更重要方法效果统一加锁顺序最有效从根本上避免死锁缩小事务范围减少锁持有时间合理使用索引减少锁的范围降低隔离级别减少间隙锁RC级别业务层重试兜底方案记住两点统一加锁顺序、缩小事务范围能解决大部分死锁问题。有问题评论区交流。