news 2026/8/8 1:30:10

MySQL死锁排查指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL死锁排查指南

MySQL死锁排查指南

  • MySQL死锁排查指南
    • 一、先搞懂:死锁是什么?
    • 二、经典场景:Java业务里的死锁长啥样?
    • 三、死锁排查:核心步骤+命令
      • 步骤1:查看死锁日志
      • 步骤2:结合Java业务定位代码
    • 四、根治死锁:Java业务里的落地方案
      • 方案1:约定统一的加锁顺序(最有效)
        • 流程展示
      • 方案2:缩短事务范围
      • 方案3:优化数据库层面(按需)

MySQL死锁排查指南

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

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

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

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

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

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

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

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

当两个事务同时执行时:

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

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

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

步骤1:查看死锁日志

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

SHOWENGINEINNODBSTATUS;

执行后,找到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 更大的账户,这就是 “统一的加锁顺序”:

@ServicepublicclassTransferService{@AutowiredprivateAccountMapperaccountMapper;// 统一的转账方法(无论谁转谁,都按ID大小顺序加锁)@Transactionalpublicvoidtransfer(StringfromId,StringtoId,intamount){// 步骤1:确定加锁顺序(全局统一规则)StringlockFirstId;// 先锁这个IDStringlockSecondId;// 后锁这个IDif(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调用(加长锁持有时间)@TransactionalpublicvoidbadTransfer(StringfromId,StringtoId,intamount){accountMapper.updateBalance(fromId,-amount);rpcClient.notifyThirdParty(fromId,toId,amount);// 非DB操作,加长事务accountMapper.updateBalance(toId,+amount);}// 好例子:事务仅包含DB操作@TransactionalpublicvoidgoodTransfer(StringfromId,StringtoId,intamount){accountMapper.updateBalance(fromId,-amount);accountMapper.updateBalance(toId,+amount);}// 非DB操作放在事务外publicvoidtransferWithNotify(StringfromId,StringtoId,intamount){goodTransfer(fromId,toId,amount);rpcClient.notifyThirdParty(fromId,toId,amount);}

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

  • 加索引:确保更新/查询的WHERE条件走索引,减少锁的范围(避免表锁);
  • 降低隔离级别:业务允许的话,将隔离级别从REPEATABLE-READ(默认)降为READ-COMMITTED,减少间隙锁;
  • 显式加锁优化:使用SELECT ... FOR UPDATE显式加锁时,确保WHERE条件走索引。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/6 18:50:55

DeepSeek V3.2 技术解读:一次不靠“堆参数”的模型升级

本文由「大千AI助手」原创发布&#xff0c;专注用真话讲AI&#xff0c;回归技术本质。拒绝神话或妖魔化。搜索「大千AI助手」关注我&#xff0c;一起撕掉过度包装&#xff0c;学习真实的AI技术&#xff01; 引言 近年来&#xff0c;大语言模型&#xff08;Large Language Mode…

作者头像 李华
网站建设 2026/8/6 16:47:39

Babel中实现ES6函数扩展的深度剖析

Babel如何让现代函数语法在旧引擎中“复活”&#xff1f;你有没有想过&#xff0c;当你写下这样一段简洁的ES6代码时&#xff1a;function greet(name Guest, ...messages) {return messages.map(msg > ${msg}, ${name}!); }它究竟是怎么在IE11这种连const都不认识的老浏览…

作者头像 李华
网站建设 2026/8/7 21:33:07

驱动开发中WinDbg分析DMP蓝屏文件的完整指南

驱动开发中的“破案神器”&#xff1a;用WinDbg精准解剖蓝屏DMP文件你有没有遇到过这样的场景&#xff1f;系统运行得好好的&#xff0c;突然“啪”一下蓝屏重启。日志里只留下一句模糊的提示&#xff1a;“意外中断”&#xff0c;或者一个看不懂的错误码0x0000007E。设备管理器…

作者头像 李华
网站建设 2026/8/5 4:22:41

Flutter AR 开发:打造厘米级精度的室内导航应用

Flutter AR 开发&#xff1a;打造厘米级精度的室内导航应用 引言 “AR 导航漂移严重&#xff0c;用户绕圈找不到出口&#xff01;” “在商场里定位误差高达 5 米&#xff0c;完全不可用&#xff01;” “手机发热严重&#xff0c;AR 模式只能坚持 3 分钟&#xff01;” ——这…

作者头像 李华
网站建设 2026/8/6 22:38:50

Flutter 与 TensorFlow Lite:在手机上实时运行 YOLOv8 目标检测

Flutter 与 TensorFlow Lite&#xff1a;在手机上实时运行 YOLOv8 目标检测 引言 “模型加载慢&#xff0c;用户等得不耐烦&#xff01;” “检测延迟高达 2 秒&#xff0c;根本没法用于扫码或安防&#xff01;” “App 体积暴涨到 300MB&#xff0c;商店审核直接拒收&#x…

作者头像 李华
网站建设 2026/8/7 23:11:11

sprintf 和 printf

sprintf 和 printf 的核心区别在于输出目的地不同&#xff1a;printf 直接将结果打印到屏幕&#xff08;如控制台&#xff09;&#xff0c;而 sprintf 将结果保存到指定的字符串缓冲区中。 &#x1f4a1; 功能与输出printf&#xff1a;格式化输出到标准输出&#xff08;通常是屏…

作者头像 李华