林淮康 發表於 2025-12-26 15:54:51

MySQL死锁排查指南

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">MySQL死锁排查指南</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">一、先搞懂:死锁是什么?</a></li><li><a href="#_lab2_0_1">二、经典场景:Java业务里的死锁长啥样?</a></li><li><a href="#_lab2_0_2">三、死锁排查:核心步骤+命令</a></li><ul class="third_class_ul"><li><a href="#_label3_0_2_0">步骤1:查看死锁日志</a></li><li><a href="#_label3_0_2_1">步骤2:结合Java业务定位代码</a></li></ul><li><a href="#_lab2_0_3">四、根治死锁:Java业务里的落地方案</a></li><ul class="third_class_ul"><li><a href="#_label3_0_3_2">方案1:约定统一的加锁顺序(最有效)</a></li><li><a href="#_label3_0_3_3">方案2:缩短事务范围</a></li><li><a href="#_label3_0_3_4">方案3:优化数据库层面(按需)</a></li></ul></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>MySQL死锁排查指南</h2>
<p>作为一名10年经验的Java工程师,我会从<strong>场景、排查、解决</strong>三个维度,带你搞定MySQL死锁问题。</p>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>一、先搞懂:死锁是什么?</h3>
<p>死锁是<strong>多个事务互相持有对方需要的资源,陷入无限等待的僵局</strong>。</p>
<p>它必须同时满足4个&ldquo;缺一不可&rdquo;的条件(破坏任意一个就能避免死锁):</p>
<ol><li><strong>资源独占</strong>:一个资源(如一行数据)只能被一个事务持有;</li><li><strong>请求并持有</strong>:事务持有资源的同时,又请求其他资源且不释放已有资源;</li><li><strong>不可剥夺</strong>:事务已获得的资源不能被强行抢占;</li><li><strong>循环等待</strong>:事务之间形成&ldquo;事务A等B,B等A&rdquo;的闭环。</li></ol>
<p class="maodian"><a name="_lab2_0_1"></a></p><h3>二、经典场景:Java业务里的死锁长啥样?</h3>
<p>以<strong>用户互转余额</strong>为例(Java+MySQL事务):</p>
<div class="jb51code"><pre class="brush:java;">// 事务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);
}</pre></div>
<p>当两个事务同时执行时:</p>
<ul><li>事务A持有A的锁,等待B的锁;</li><li>事务B持有B的锁,等待A的锁;<br />&rarr; 死锁产生。</li></ul>
<p class="maodian"><a name="_lab2_0_2"></a></p><h3>三、死锁排查:核心步骤+命令</h3>
<p>当业务出现&ldquo;接口超时、事务卡住&rdquo;时,优先排查死锁。</p>
<p class="maodian"><a name="_label3_0_2_0"></a></p><h4>步骤1:查看死锁日志</h4>
<p>MySQL(InnoDB引擎)最核心的排查命令:</p>
<div class="jb51code"><pre class="brush:bash;">SHOW ENGINE INNODB STATUS;</pre></div>
<p>执行后,找到 <code>LATEST DETECTED DEADLOCK</code> 模块,关键信息包括:</p>
<ul><li><code>TRANSACTION (1)/(2)</code>:冲突的两个事务;</li><li><code>WAITING FOR THIS LOCK</code>:事务等待的锁及对应的SQL;</li><li><code>HOLDS THE LOCK(S)</code>:事务持有的锁及对应的SQL;</li><li><code>WE ROLL BACK TRANSACTION (X)</code>:MySQL自动回滚的事务(解决死锁)。</li></ul>
<p class="maodian"><a name="_label3_0_2_1"></a></p><h4>步骤2:结合Java业务定位代码</h4>
<p>根据死锁日志里的<strong>SQL语句</strong>,找到对应的Java代码(比如上述<code>transferAtoB</code>方法),分析事务的加锁顺序是否不一致。</p>
<p class="maodian"><a name="_lab2_0_3"></a></p><h3>四、根治死锁:Java业务里的落地方案</h3>
<p>针对Java业务,从<strong>代码、数据库</strong>两个层面解决:</p>
<p class="maodian"><a name="_label3_0_3_2"></a></p><h4>方案1:约定统一的加锁顺序(最有效)</h4>
<p>我们约定一个全局规则:无论转账方向如何,都先锁定 ID 字典序更小的账户,再锁定 ID 更大的账户,这就是 &ldquo;统一的加锁顺序&rdquo;:</p>
<div class="jb51code"><pre class="brush:java;">@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) &lt; 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); // 转入
      }
    }
}</pre></div>
<p>假设:A 的 ID 是user_001,B 的 ID 是user_002(user_001 &lt; user_002)。</p>
<ul><li>当调用transfer(&ldquo;user_001&rdquo;, &ldquo;user_002&rdquo;, 10)(A 转 B):先锁user_001,再锁user_002;</li><li>当调用transfer(&ldquo;user_002&rdquo;, &ldquo;user_001&rdquo;, 20)(B 转 A):依然先锁user_001,再锁user_002;<br />两个事务的加锁顺序完全一致,不会出现 &ldquo;你等我、我等你&rdquo; 的循环等待,从根源上杜绝死锁。</li></ul>
<h5>流程展示</h5>
<ul><li>用户A:ID为 <code>user_001</code></li><li>用户B:ID为 <code>user_002</code></li><li>规则:<code>user_001</code> 的字典序 &lt; <code>user_002</code></li></ul>
<p>无统一加锁顺序 &rarr; 死锁(执行流程)<br />当两个事务<strong>各自按&ldquo;转出方&rarr;转入方&rdquo;的顺序加锁</strong>时:</p>
<table><thead><tr><th>时间线</th><th>事务1(A转B:先锁A,再锁B)</th><th>事务2(B转A:先锁B,再锁A)</th><th>状态</th></tr></thead><tbody><tr><td>T1</td><td>执行 <code>deductBalance(&quot;user_001&quot;, 10)</code>,成功锁定 <code>user_001</code></td><td>-</td><td>事务1持有A的锁</td></tr><tr><td>T2</td><td>-</td><td>执行 <code>deductBalance(&quot;user_002&quot;, 20)</code>,成功锁定 <code>user_002</code></td><td>事务2持有B的锁</td></tr><tr><td>T3</td><td>尝试执行 <code>addBalance(&quot;user_002&quot;, 10)</code>,需要锁B &rarr; 等待</td><td>-</td><td>事务1等待B的锁</td></tr><tr><td>T4</td><td>-</td><td>尝试执行 <code>addBalance(&quot;user_001&quot;, 20)</code>,需要锁A &rarr; 等待</td><td>事务2等待A的锁</td></tr><tr><td>T5</td><td>持续等待B的锁</td><td>持续等待A的锁</td><td><strong>死锁</strong></td></tr></tbody></table>
<p>有统一加锁顺序 &rarr; 无死锁(执行流程)<br />当两个事务<strong>都按&ldquo;ID从小到大&rdquo;的顺序加锁</strong>时:</p>
<table><thead><tr><th>时间线</th><th>事务1(A转B:先锁A,再锁B)</th><th>事务2(B转A:先锁A,再锁B)</th><th>状态</th></tr></thead><tbody><tr><td>T1</td><td>执行 <code>deductBalance(&quot;user_001&quot;, 10)</code>,成功锁定 <code>user_001</code></td><td>-</td><td>事务1持有A的锁</td></tr><tr><td>T2</td><td>-</td><td>尝试执行 <code>addBalance(&quot;user_001&quot;, 20)</code>,需要锁A &rarr; 等待</td><td>事务2等待A的锁</td></tr><tr><td>T3</td><td>执行 <code>addBalance(&quot;user_002&quot;, 10)</code>,成功锁定 <code>user_002</code></td><td>-</td><td>事务1持有A、B的锁</td></tr><tr><td>T4</td><td>事务执行完成,释放A、B的锁</td><td>-</td><td>事务1提交,锁释放</td></tr><tr><td>T5</td><td>-</td><td>获得A的锁,执行 <code>addBalance(&quot;user_001&quot;, 20)</code></td><td>事务2持有A的锁</td></tr><tr><td>T6</td><td>-</td><td>执行 <code>deductBalance(&quot;user_002&quot;, 20)</code>,成功锁定 <code>user_002</code></td><td>事务2持有A、B的锁</td></tr><tr><td>T7</td><td>-</td><td>事务执行完成,释放A、B的锁</td><td>事务2提交,无死锁</td></tr></tbody></table>
<p>这样是不是更清楚了?需要我把这个流程做成更简洁的<strong>对比表格</strong>方便你保存吗?</p>
<p class="maodian"><a name="_label3_0_3_3"></a></p><h4>方案2:缩短事务范围</h4>
<p>避免事务中包含非数据库操作(如RPC调用、日志打印),减少锁的持有时间:</p>
<div class="jb51code"><pre class="brush:java;">// 坏例子:事务包含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);
}</pre></div>
<p class="maodian"><a name="_label3_0_3_4"></a></p><h4>方案3:优化数据库层面(按需)</h4>
<ul><li><strong>加索引</strong>:确保更新/查询的<code>WHERE</code>条件走索引,减少锁的范围(避免表锁);</li><li><strong>降低隔离级别</strong>:业务允许的话,将隔离级别从<code>REPEATABLE-READ</code>(默认)降为<code>READ-COMMITTED</code>,减少间隙锁;</li><li><strong>显式加锁优化</strong>:使用<code>SELECT ... FOR UPDATE</code>显式加锁时,确保<code>WHERE</code>条件走索引。</li></ul>
頁: [1]
查看完整版本: MySQL死锁排查指南