MySQL死锁排查与预防实战

 更新时间:2026年02月11日 08:54:56   作者:花宝宝hua  
死锁是MySQL高并发场景下的常见问题,偶尔一两次可以通过业务重试解决,本文就来详细的介绍一下MySQL死锁排查与预防实战,感兴趣的可以了解一下

前言

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

Deadlock found when trying to get lock; try restarting transaction

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

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

一、查看死锁信息

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

SHOW ENGINE INNODB STATUS\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
@Transactional
public void processOrder(long orderId, long productId) {
    updateOrderStatus(orderId, "paid");  // 永远先锁orders
    decreaseInventory(productId, 1);     // 再锁inventory
}

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

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

四、间隙锁导致的死锁

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

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

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

-- 事务A
SELECT * FROM orders WHERE user_id = 3 FOR UPDATE;
-- 没有user_id=3的数据,但会锁住(1,5)这个间隙

-- 事务B
SELECT * FROM orders WHERE user_id = 7 FOR UPDATE;
-- 锁住(5,10)这个间隙

-- 然后两边各自INSERT
-- 事务A想插入user_id=6,要等(5,10)的间隙锁
-- 事务B想插入user_id=4,要等(1,5)的间隙锁
-- 死锁

解决办法:

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

五、缩小事务范围

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

// 这种写法不好
@Transactional
public void process() {
    queryData();        // 查数据
    callExternalApi();  // 调外部接口,可能很慢
    updateDatabase();   // 更新数据库
}

// 改成这样
public void process() {
    queryData();
    callExternalApi();  // 外部调用放事务外面
    updateInTransaction();
}

@Transactional
public void updateInTransaction() {
    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: MySQLDeadlock
  expr: increase(mysql_global_status_innodb_deadlocks[5m]) > 0
  for: 1m

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

七、业务层重试

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

int retry = 3;
while (retry-- > 0) {
    try {
        doTransaction();
        break;
    } catch (DeadlockException e) {
        if (retry == 0) throw e;
        Thread.sleep(100);  // 等一下再试
    }
}

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

总结

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

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

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

到此这篇关于MySQL死锁排查与预防实战的文章就介绍到这了,更多相关MySQL死锁排查内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 通过两种方式增加从库——不停止mysql服务

    通过两种方式增加从库——不停止mysql服务

    现在生产环境MySQL数据库是一主一从,由于业务量访问不断增大,故再增加一台从库。前提是不能影响线上业务使用,也就是说不能重启MySQL服务,为了避免出现其他情况,选择在网站访问量低峰期时间段操作
    2015-11-11
  • 利用MySQL函数实现判断视频扩展名的代码

    利用MySQL函数实现判断视频扩展名的代码

    MySQL拥有强大的自定义函数功能,如下,我写了一个用MySQL函数 判断视频地址是否可以手机端播放
    2012-02-02
  • MySQL多实例管理如何在一台主机上运行多个mysql

    MySQL多实例管理如何在一台主机上运行多个mysql

    文章详解了在Linux主机上通过二进制方式安装MySQL多实例的步骤,涵盖端口配置、数据目录准备、初始化与启动流程,以及排错方法,适用于构建读写分离架构,感兴趣的朋友一起看看吧
    2025-07-07
  • mysql数据库提权的三种方法

    mysql数据库提权的三种方法

    文介绍了MySQL数据库的三种提权方法:UDF提权、MOF提权和启动项提权,同时列出了一些常见数据库及其默认端口,下面就来介绍一下,感兴趣的可以了解一下
    2024-09-09
  • MySQL索引总结(Index Type)

    MySQL索引总结(Index Type)

    本文主要介绍了MySQL索引总结(Index Type),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-02-02
  • mysql数据存放的位置在哪

    mysql数据存放的位置在哪

    在本篇文章里小编给大家分享的是关于mysql数据存放的位置及相关知识点内容,需要的朋友们可以参考下。
    2020-07-07
  • MySQL 数据类型 详解

    MySQL 数据类型 详解

    MySQL 的数值数据类型可以大致划分为两个类别,一个是整数,另一个是浮点数或小数。许多不同的子类型对这些类别中的每一个都是可用的,每个子类型支持不同大小的数据,并且 MySQL 允许我们指定数值字段中的值是否有正负之分或者用零填补。
    2009-10-10
  • MySQL中使用PROFILING来查看SQL执行流程的实现步骤

    MySQL中使用PROFILING来查看SQL执行流程的实现步骤

    在MySQL中,PROFILING功能提供了一种方式来分析SQL语句的执行时间,包括查询执行的各个阶段,如发送、解析、优化、执行等,这对于诊断性能问题非常有用,本文给大家介绍了MySQL中使用PROFILING来查看SQL执行流程的实现步骤,需要的朋友可以参考下
    2024-07-07
  • MySQL多表关联查询方式及实际应用

    MySQL多表关联查询方式及实际应用

    MySQL语句学习的难点和重点就在于多表查询,同时MySQL也有诸多方法供大家选择,不论是多表联查(联结表、左连接、右连接……),这篇文章主要给大家介绍了关于MySQL多表关联查询方式及实际应用的相关资料,需要的朋友可以参考下
    2024-07-07
  • MySQL中的各种查询问题

    MySQL中的各种查询问题

    这篇文章主要介绍了MySQL中的各种查询问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01

最新评论