数据库死锁排查和解决方案

数据库死锁是并发编程中非常经典的问题。简单来说,就是两个或多个事务互相持有对方想要的锁,并且都不肯放手,导致程序无限期卡死。

下面我为你提供一个最经典的数据库死锁代码示例,并详细讲解如何排查和解决它。

💻 导致死锁的代码示例

这段代码模拟了两个事务以相反的顺序去更新两行数据(账户A和账户B),从而引发死锁。这在转账等涉及多行数据更新的业务中非常常见:

// 事务A:用户A给用户B转账
@Transactional
public void transferAtoB(String aId, String bId, int amount) {
    // 1. 先锁定并扣减A账户的余额
    accountMapper.updateBalance(aId, -amount);
    // 2. 尝试锁定并增加B账户的余额(如果此时事务B正在操作,这里就会陷入等待)
    accountMapper.updateBalance(bId, +amount);
}

// 事务B:用户B给用户A转账
@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账户,形成循环等待,数据库就会报出死锁错误。


🔍 如何排查数据库死锁?

死锁发生时,数据库通常会主动回滚其中一个事务(牺牲品),并抛出类似 Deadlock found when trying to get lock 的错误。我们可以通过以下核心步骤来定位问题:

1. 查看数据库死锁日志(最核心)

以 MySQL (InnoDB引擎) 为例,排查死锁最直接的方法就是查看它自动记录的死锁详情。

  • 第一步:连接到你的数据库,执行核心命令:SHOW ENGINE INNODB STATUS;
  • 第二步:在输出的海量信息中,找到 LATEST DETECTED DEADLOCK 这一部分。
  • 第三步:分析日志。这部分会非常清晰地展示:
    • 发生死锁的两个事务分别执行了什么 SQL 语句。
    • 每个事务当前持有了哪些锁(HOLDS THE LOCK(S))。
    • 每个事务正在等待哪些锁(WAITING FOR THIS LOCK)。

2. 开启死锁日志持久化(方便后续复盘)

默认情况下,死锁信息只保存在内存中,数据库重启后会丢失。为了方便后续排查,建议在 MySQL 的配置文件(my.cnf)中加入以下配置,将死锁信息持久化到错误日志中:

innodb_print_all_deadlocks = 1
log_error = /var/log/mysql/error.log

配置完成后重启数据库,所有的死锁记录都会被写入日志文件。


🛠️ 如何解决和预防数据库死锁?

死锁的产生必须同时满足四个条件(互斥、持有并等待、不可剥夺、循环等待)。我们只要破坏其中任意一个条件,就能避免死锁。以下是四种最实用的解决方案:

方案一:统一资源的访问顺序(破坏“循环等待”条件)⭐ 最推荐

只要所有事务都按照相同的顺序去获取锁,就绝对不会发生死锁。

  • 修改思路:在业务代码中强制规定,无论转账方向如何,都必须先锁定 ID 字典序(或数字大小)更小的账户,再锁定 ID 更大的账户。
  • 代码修改:把上面代码中的逻辑改掉,在 transfer 方法中先比较 aIdbId 的大小,然后严格按照先小后大的顺序去执行 updateBalance,死锁立刻消失。

方案二:缩短事务的持有时间(破坏“持有并等待”条件)

事务持有锁的时间越长,发生死锁的概率就越高。

  • 优化建议
    • 将大事务拆分为小事务(例如批量更新时,分批次提交)。
    • 绝对不要在数据库事务中执行耗时的无关操作,比如调用外部 HTTP 接口、进行复杂的远程计算或等待用户输入。

方案三:使用乐观锁替代悲观锁

对于并发量高、但写冲突较少的场景,可以使用乐观锁来彻底避免加锁带来的死锁风险。

  • 实现思路:在表中增加一个 version 版本号字段。更新数据时,不直接加排他锁,而是校验版本号。
  • SQL 示例
-- 更新时带上版本号条件
UPDATE user_balance 
SET balance = balance - 100, version = version + 1 
WHERE id = 1 AND version = 1;

如果更新返回的影响行数为 0,说明数据已经被其他事务修改过,此时业务层只需要重新查询并尝试更新即可。

方案四:优化索引,缩小锁的粒度

如果 SQL 语句没有走索引,数据库在执行更新或删除时可能会进行全表扫描,导致锁住大量不相关的行甚至整张表,极大地增加了死锁概率。

  • 优化建议:确保 UPDATEDELETE 语句的 WHERE 条件字段都建立了合适的索引,让数据库能够精准地锁定目标行,减少锁冲突。

你可以先在本地模拟上面的转账死锁场景,然后用 SHOW ENGINE INNODB STATUS; 亲自排查一遍,熟悉死锁日志的特征,以后在线上遇到就能快速定位了!

posted @ 2026-05-06 21:15  圣祖帝皇  阅读(41)  评论(0)    收藏  举报