mysql死锁排查指南
作为一名10年经验的java工程师,我会从场景、排查、解决三个维度,带你搞定mysql死锁问题。
一、先搞懂:死锁是什么?
死锁是多个事务互相持有对方需要的资源,陷入无限等待的僵局。
它必须同时满足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死锁排查内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论