MySQL死锁排查指南

 更新时间:2025年12月26日 15:54:51   作者:不如打代码KK  
本文详细介绍了MySQL死锁的排查与解决方法,从死锁的定义、Java业务中的死锁场景、死锁排查步骤和命令到根治死锁的方案,包括约定统一的加锁顺序、缩短事务范围和优化数据库层面,提供了全面的指导,感兴趣的朋友跟随小编一起看看吧

MySQL死锁排查指南

作为一名10年经验的Java工程师,我会从场景、排查、解决三个维度,带你搞定MySQL死锁问题。

一、先搞懂:死锁是什么?

死锁是多个事务互相持有对方需要的资源,陷入无限等待的僵局

它必须同时满足4个“缺一不可”的条件(破坏任意一个就能避免死锁):

  1. 资源独占:一个资源(如一行数据)只能被一个事务持有;
  2. 请求并持有:事务持有资源的同时,又请求其他资源且不释放已有资源;
  3. 不可剥夺:事务已获得的资源不能被强行抢占;
  4. 循环等待:事务之间形成“事务A等B,B等A”的闭环。

二、经典场景:Java业务里的死锁长啥样?

用户互转余额为例(Java+MySQL事务):

// 事务A:用户A给B转账10元
@Transactional
public void transferAtoB(String aId, String bId, int amount) {
    // 1. 锁定A的账户(更新操作会加行锁)
    accountMapper.updateBalance(aId, -amount);
    // 2. 尝试锁定B的账户(若B此时正在操作A,就会等待)
    accountMapper.updateBalance(bId, +amount);
}
// 事务B:用户B给A转账20元
@Transactional
public void transferBtoA(String bId, String aId, int amount) {
    // 1. 锁定B的账户
    accountMapper.updateBalance(bId, -amount);
    // 2. 尝试锁定A的账户(此时A已被事务A锁定,陷入等待)
    accountMapper.updateBalance(aId, +amount);
}

当两个事务同时执行时:

  • 事务A持有A的锁,等待B的锁;
  • 事务B持有B的锁,等待A的锁;
    → 死锁产生。

三、死锁排查:核心步骤+命令

当业务出现“接口超时、事务卡住”时,优先排查死锁。

步骤1:查看死锁日志

MySQL(InnoDB引擎)最核心的排查命令:

SHOW ENGINE INNODB STATUS;

执行后,找到 LATEST DETECTED DEADLOCK 模块,关键信息包括:

  • TRANSACTION (1)/(2):冲突的两个事务;
  • WAITING FOR THIS LOCK:事务等待的锁及对应的SQL;
  • HOLDS THE LOCK(S):事务持有的锁及对应的SQL;
  • WE ROLL BACK TRANSACTION (X):MySQL自动回滚的事务(解决死锁)。

步骤2:结合Java业务定位代码

根据死锁日志里的SQL语句,找到对应的Java代码(比如上述transferAtoB方法),分析事务的加锁顺序是否不一致。

四、根治死锁:Java业务里的落地方案

针对Java业务,从代码、数据库两个层面解决:

方案1:约定统一的加锁顺序(最有效)

我们约定一个全局规则:无论转账方向如何,都先锁定 ID 字典序更小的账户,再锁定 ID 更大的账户,这就是 “统一的加锁顺序”:

@Service
public class TransferService {
    @Autowired
    private AccountMapper accountMapper;
    // 统一的转账方法(无论谁转谁,都按ID大小顺序加锁)
    @Transactional
    public void transfer(String fromId, String toId, int amount) {
        // 步骤1:确定加锁顺序(全局统一规则)
        String lockFirstId; // 先锁这个ID
        String lockSecondId; // 后锁这个ID
        if (fromId.compareTo(toId) < 0) {
            lockFirstId = fromId;
            lockSecondId = toId;
        } else {
            lockFirstId = toId;
            lockSecondId = fromId;
        }
        // 步骤2:按统一顺序加锁(先锁小ID,再锁大ID)
        // 先锁定第一个账户(无论它是转出方还是转入方)
        if (lockFirstId.equals(fromId)) {
            accountMapper.deductBalance(lockFirstId, amount); // 转出
        } else {
            accountMapper.addBalance(lockFirstId, amount); // 转入
        }
        // 再锁定第二个账户
        if (lockSecondId.equals(fromId)) {
            accountMapper.deductBalance(lockSecondId, amount); // 转出
        } else {
            accountMapper.addBalance(lockSecondId, amount); // 转入
        }
    }
}

假设:A 的 ID 是user_001,B 的 ID 是user_002(user_001 < user_002)。

  • 当调用transfer(“user_001”, “user_002”, 10)(A 转 B):先锁user_001,再锁user_002;
  • 当调用transfer(“user_002”, “user_001”, 20)(B 转 A):依然先锁user_001,再锁user_002;
    两个事务的加锁顺序完全一致,不会出现 “你等我、我等你” 的循环等待,从根源上杜绝死锁。
流程展示
  • 用户A:ID为 user_001
  • 用户B:ID为 user_002
  • 规则:user_001 的字典序 < user_002

无统一加锁顺序 → 死锁(执行流程)
当两个事务各自按“转出方→转入方”的顺序加锁时:

时间线事务1(A转B:先锁A,再锁B)事务2(B转A:先锁B,再锁A)状态
T1执行 deductBalance("user_001", 10),成功锁定 user_001-事务1持有A的锁
T2-执行 deductBalance("user_002", 20),成功锁定 user_002事务2持有B的锁
T3尝试执行 addBalance("user_002", 10),需要锁B → 等待-事务1等待B的锁
T4-尝试执行 addBalance("user_001", 20),需要锁A → 等待事务2等待A的锁
T5持续等待B的锁持续等待A的锁死锁

有统一加锁顺序 → 无死锁(执行流程)
当两个事务都按“ID从小到大”的顺序加锁时:

时间线事务1(A转B:先锁A,再锁B)事务2(B转A:先锁A,再锁B)状态
T1执行 deductBalance("user_001", 10),成功锁定 user_001-事务1持有A的锁
T2-尝试执行 addBalance("user_001", 20),需要锁A → 等待事务2等待A的锁
T3执行 addBalance("user_002", 10),成功锁定 user_002-事务1持有A、B的锁
T4事务执行完成,释放A、B的锁-事务1提交,锁释放
T5-获得A的锁,执行 addBalance("user_001", 20)事务2持有A的锁
T6-执行 deductBalance("user_002", 20),成功锁定 user_002事务2持有A、B的锁
T7-事务执行完成,释放A、B的锁事务2提交,无死锁

这样是不是更清楚了?需要我把这个流程做成更简洁的对比表格方便你保存吗?

方案2:缩短事务范围

避免事务中包含非数据库操作(如RPC调用、日志打印),减少锁的持有时间:

// 坏例子:事务包含RPC调用(加长锁持有时间)
@Transactional
public void badTransfer(String fromId, String toId, int amount) {
    accountMapper.updateBalance(fromId, -amount);
    rpcClient.notifyThirdParty(fromId, toId, amount); // 非DB操作,加长事务
    accountMapper.updateBalance(toId, +amount);
}
// 好例子:事务仅包含DB操作
@Transactional
public void goodTransfer(String fromId, String toId, int amount) {
    accountMapper.updateBalance(fromId, -amount);
    accountMapper.updateBalance(toId, +amount);
}
// 非DB操作放在事务外
public void transferWithNotify(String fromId, String toId, int amount) {
    goodTransfer(fromId, toId, amount);
    rpcClient.notifyThirdParty(fromId, toId, amount);
}

方案3:优化数据库层面(按需)

  • 加索引:确保更新/查询的WHERE条件走索引,减少锁的范围(避免表锁);
  • 降低隔离级别:业务允许的话,将隔离级别从REPEATABLE-READ(默认)降为READ-COMMITTED,减少间隙锁;
  • 显式加锁优化:使用SELECT ... FOR UPDATE显式加锁时,确保WHERE条件走索引。

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

相关文章

  • MySQL遇到“ Access denied for user ”问题的解决办法

    MySQL遇到“ Access denied for user ”问题的解决办法

    这篇文章主要介绍了MySQL遇到“ Access denied for user ”问题的解决办法,文中通过代码示例讲解的非常详细,对大家的解决问题有一定的帮助,需要的朋友可以参考下
    2024-12-12
  • MySQL8.2.0安装教程分享

    MySQL8.2.0安装教程分享

    这篇文章详细介绍了如何在Windows系统上安装MySQL数据库软件,包括下载、安装、配置和设置环境变量的步骤
    2025-02-02
  • MySQL存储过程的传参和流程控制示例讲解

    MySQL存储过程的传参和流程控制示例讲解

    这篇文章主要介绍了MySQL存储过程的传参和流程控制示例讲解, repeat和Loop区别是repeat有自己退出循环的语句until,Loop使用的是if判断语句,本文结合示例代码详细讲解,需要的朋友可以参考下
    2023-02-02
  • mysql导出导入中文表解决方法

    mysql导出导入中文表解决方法

    在开发过程中会经常用到mysql导出导入中文表,本文将详细介绍其如何使用,需要的朋友可以参考下
    2012-11-11
  • mysql 5.7.13 安装配置笔记(Mac os)

    mysql 5.7.13 安装配置笔记(Mac os)

    这篇文章主要为大家详细介绍了Mac os下mysql 5.7.13 安装配置方法教程,感兴趣的小伙伴们可以参考一下
    2016-06-06
  • MySQL自定义序列数的实现方式

    MySQL自定义序列数的实现方式

    这篇文章主要介绍了MySQL自定义序列数的实现方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • 在Centos 5.5 上编译安装mysql 5.5.9

    在Centos 5.5 上编译安装mysql 5.5.9

    由于开发需要,现在需要安装安装MySQL 5.5.9,使用了rpm安装总是出错,而且还有好多依事关系麻烦,此外也没有找到二进制的包,只好找源码包进行编译;
    2014-07-07
  • MySQL中通过SQL语句删除重复记录并且只保留一条记录

    MySQL中通过SQL语句删除重复记录并且只保留一条记录

    本文主要介绍了MySQL中通过SQL语句删除重复记录并且只保留一条记录,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01
  • MySQL 批量插入的原理和实战方法(快速提升大数据导入效率)

    MySQL 批量插入的原理和实战方法(快速提升大数据导入效率)

    在日常开发中,我们经常需要将大量数据批量插入到 MySQL 数据库中,本文将介绍批量插入的原理、实现方法,并结合 Python 和 PyMySQL 库提供详细的实战示例,感兴趣的朋友跟随小编一起看看吧
    2025-11-11
  • 从0到1学会MySQL单表查询

    从0到1学会MySQL单表查询

    这篇文章主要给大家介绍了关于如何从0到1学会MySQL单表查询的相关资料,单表查询是指从一张表数据中查询所需的数据,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-11-11

最新评论