MySQL死鎖排查指南
MySQL死鎖排查指南
作為一名10年經(jīng)驗(yàn)的Java工程師,我會(huì)從場(chǎng)景、排查、解決三個(gè)維度,帶你搞定MySQL死鎖問(wèn)題。
一、先搞懂:死鎖是什么?
死鎖是多個(gè)事務(wù)互相持有對(duì)方需要的資源,陷入無(wú)限等待的僵局。
它必須同時(shí)滿足4個(gè)“缺一不可”的條件(破壞任意一個(gè)就能避免死鎖):
- 資源獨(dú)占:一個(gè)資源(如一行數(shù)據(jù))只能被一個(gè)事務(wù)持有;
- 請(qǐng)求并持有:事務(wù)持有資源的同時(shí),又請(qǐng)求其他資源且不釋放已有資源;
- 不可剝奪:事務(wù)已獲得的資源不能被強(qiáng)行搶占;
- 循環(huán)等待:事務(wù)之間形成“事務(wù)A等B,B等A”的閉環(huán)。
二、經(jīng)典場(chǎng)景:Java業(yè)務(wù)里的死鎖長(zhǎng)啥樣?
以用戶互轉(zhuǎn)余額為例(Java+MySQL事務(wù)):
// 事務(wù)A:用戶A給B轉(zhuǎn)賬10元
@Transactional
public void transferAtoB(String aId, String bId, int amount) {
// 1. 鎖定A的賬戶(更新操作會(huì)加行鎖)
accountMapper.updateBalance(aId, -amount);
// 2. 嘗試鎖定B的賬戶(若B此時(shí)正在操作A,就會(huì)等待)
accountMapper.updateBalance(bId, +amount);
}
// 事務(wù)B:用戶B給A轉(zhuǎn)賬20元
@Transactional
public void transferBtoA(String bId, String aId, int amount) {
// 1. 鎖定B的賬戶
accountMapper.updateBalance(bId, -amount);
// 2. 嘗試鎖定A的賬戶(此時(shí)A已被事務(wù)A鎖定,陷入等待)
accountMapper.updateBalance(aId, +amount);
}當(dāng)兩個(gè)事務(wù)同時(shí)執(zhí)行時(shí):
- 事務(wù)A持有A的鎖,等待B的鎖;
- 事務(wù)B持有B的鎖,等待A的鎖;
→ 死鎖產(chǎn)生。
三、死鎖排查:核心步驟+命令
當(dāng)業(yè)務(wù)出現(xiàn)“接口超時(shí)、事務(wù)卡住”時(shí),優(yōu)先排查死鎖。
步驟1:查看死鎖日志
MySQL(InnoDB引擎)最核心的排查命令:
SHOW ENGINE INNODB STATUS;
執(zhí)行后,找到 LATEST DETECTED DEADLOCK 模塊,關(guān)鍵信息包括:
TRANSACTION (1)/(2):沖突的兩個(gè)事務(wù);WAITING FOR THIS LOCK:事務(wù)等待的鎖及對(duì)應(yīng)的SQL;HOLDS THE LOCK(S):事務(wù)持有的鎖及對(duì)應(yīng)的SQL;WE ROLL BACK TRANSACTION (X):MySQL自動(dòng)回滾的事務(wù)(解決死鎖)。
步驟2:結(jié)合Java業(yè)務(wù)定位代碼
根據(jù)死鎖日志里的SQL語(yǔ)句,找到對(duì)應(yīng)的Java代碼(比如上述transferAtoB方法),分析事務(wù)的加鎖順序是否不一致。
四、根治死鎖:Java業(yè)務(wù)里的落地方案
針對(duì)Java業(yè)務(wù),從代碼、數(shù)據(jù)庫(kù)兩個(gè)層面解決:
方案1:約定統(tǒng)一的加鎖順序(最有效)
我們約定一個(gè)全局規(guī)則:無(wú)論轉(zhuǎn)賬方向如何,都先鎖定 ID 字典序更小的賬戶,再鎖定 ID 更大的賬戶,這就是 “統(tǒng)一的加鎖順序”:
@Service
public class TransferService {
@Autowired
private AccountMapper accountMapper;
// 統(tǒng)一的轉(zhuǎn)賬方法(無(wú)論誰(shuí)轉(zhuǎn)誰(shuí),都按ID大小順序加鎖)
@Transactional
public void transfer(String fromId, String toId, int amount) {
// 步驟1:確定加鎖順序(全局統(tǒng)一規(guī)則)
String lockFirstId; // 先鎖這個(gè)ID
String lockSecondId; // 后鎖這個(gè)ID
if (fromId.compareTo(toId) < 0) {
lockFirstId = fromId;
lockSecondId = toId;
} else {
lockFirstId = toId;
lockSecondId = fromId;
}
// 步驟2:按統(tǒng)一順序加鎖(先鎖小ID,再鎖大ID)
// 先鎖定第一個(gè)賬戶(無(wú)論它是轉(zhuǎn)出方還是轉(zhuǎn)入方)
if (lockFirstId.equals(fromId)) {
accountMapper.deductBalance(lockFirstId, amount); // 轉(zhuǎn)出
} else {
accountMapper.addBalance(lockFirstId, amount); // 轉(zhuǎn)入
}
// 再鎖定第二個(gè)賬戶
if (lockSecondId.equals(fromId)) {
accountMapper.deductBalance(lockSecondId, amount); // 轉(zhuǎn)出
} else {
accountMapper.addBalance(lockSecondId, amount); // 轉(zhuǎn)入
}
}
}假設(shè):A 的 ID 是user_001,B 的 ID 是user_002(user_001 < user_002)。
- 當(dāng)調(diào)用transfer(“user_001”, “user_002”, 10)(A 轉(zhuǎn) B):先鎖user_001,再鎖user_002;
- 當(dāng)調(diào)用transfer(“user_002”, “user_001”, 20)(B 轉(zhuǎn) A):依然先鎖user_001,再鎖user_002;
兩個(gè)事務(wù)的加鎖順序完全一致,不會(huì)出現(xiàn) “你等我、我等你” 的循環(huán)等待,從根源上杜絕死鎖。
流程展示
- 用戶A:ID為
user_001 - 用戶B:ID為
user_002 - 規(guī)則:
user_001的字典序 <user_002
無(wú)統(tǒng)一加鎖順序 → 死鎖(執(zhí)行流程)
當(dāng)兩個(gè)事務(wù)各自按“轉(zhuǎn)出方→轉(zhuǎn)入方”的順序加鎖時(shí):
| 時(shí)間線 | 事務(wù)1(A轉(zhuǎn)B:先鎖A,再鎖B) | 事務(wù)2(B轉(zhuǎn)A:先鎖B,再鎖A) | 狀態(tài) |
|---|---|---|---|
| T1 | 執(zhí)行 deductBalance("user_001", 10),成功鎖定 user_001 | - | 事務(wù)1持有A的鎖 |
| T2 | - | 執(zhí)行 deductBalance("user_002", 20),成功鎖定 user_002 | 事務(wù)2持有B的鎖 |
| T3 | 嘗試執(zhí)行 addBalance("user_002", 10),需要鎖B → 等待 | - | 事務(wù)1等待B的鎖 |
| T4 | - | 嘗試執(zhí)行 addBalance("user_001", 20),需要鎖A → 等待 | 事務(wù)2等待A的鎖 |
| T5 | 持續(xù)等待B的鎖 | 持續(xù)等待A的鎖 | 死鎖 |
有統(tǒng)一加鎖順序 → 無(wú)死鎖(執(zhí)行流程)
當(dāng)兩個(gè)事務(wù)都按“ID從小到大”的順序加鎖時(shí):
| 時(shí)間線 | 事務(wù)1(A轉(zhuǎn)B:先鎖A,再鎖B) | 事務(wù)2(B轉(zhuǎn)A:先鎖A,再鎖B) | 狀態(tài) |
|---|---|---|---|
| T1 | 執(zhí)行 deductBalance("user_001", 10),成功鎖定 user_001 | - | 事務(wù)1持有A的鎖 |
| T2 | - | 嘗試執(zhí)行 addBalance("user_001", 20),需要鎖A → 等待 | 事務(wù)2等待A的鎖 |
| T3 | 執(zhí)行 addBalance("user_002", 10),成功鎖定 user_002 | - | 事務(wù)1持有A、B的鎖 |
| T4 | 事務(wù)執(zhí)行完成,釋放A、B的鎖 | - | 事務(wù)1提交,鎖釋放 |
| T5 | - | 獲得A的鎖,執(zhí)行 addBalance("user_001", 20) | 事務(wù)2持有A的鎖 |
| T6 | - | 執(zhí)行 deductBalance("user_002", 20),成功鎖定 user_002 | 事務(wù)2持有A、B的鎖 |
| T7 | - | 事務(wù)執(zhí)行完成,釋放A、B的鎖 | 事務(wù)2提交,無(wú)死鎖 |
這樣是不是更清楚了?需要我把這個(gè)流程做成更簡(jiǎn)潔的對(duì)比表格方便你保存嗎?
方案2:縮短事務(wù)范圍
避免事務(wù)中包含非數(shù)據(jù)庫(kù)操作(如RPC調(diào)用、日志打?。?,減少鎖的持有時(shí)間:
// 壞例子:事務(wù)包含RPC調(diào)用(加長(zhǎng)鎖持有時(shí)間)
@Transactional
public void badTransfer(String fromId, String toId, int amount) {
accountMapper.updateBalance(fromId, -amount);
rpcClient.notifyThirdParty(fromId, toId, amount); // 非DB操作,加長(zhǎng)事務(wù)
accountMapper.updateBalance(toId, +amount);
}
// 好例子:事務(wù)僅包含DB操作
@Transactional
public void goodTransfer(String fromId, String toId, int amount) {
accountMapper.updateBalance(fromId, -amount);
accountMapper.updateBalance(toId, +amount);
}
// 非DB操作放在事務(wù)外
public void transferWithNotify(String fromId, String toId, int amount) {
goodTransfer(fromId, toId, amount);
rpcClient.notifyThirdParty(fromId, toId, amount);
}方案3:優(yōu)化數(shù)據(jù)庫(kù)層面(按需)
- 加索引:確保更新/查詢的
WHERE條件走索引,減少鎖的范圍(避免表鎖); - 降低隔離級(jí)別:業(yè)務(wù)允許的話,將隔離級(jí)別從
REPEATABLE-READ(默認(rèn))降為READ-COMMITTED,減少間隙鎖; - 顯式加鎖優(yōu)化:使用
SELECT ... FOR UPDATE顯式加鎖時(shí),確保WHERE條件走索引。
到此這篇關(guān)于MySQL死鎖排查指南的文章就介紹到這了,更多相關(guān)mysql死鎖排查內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mysql創(chuàng)建子賬號(hào)及權(quán)限設(shè)置全過(guò)程
本文介紹通過(guò)Xshell6連接MySQL,創(chuàng)建test子賬號(hào)并設(shè)置密碼,測(cè)試登錄,分配本地及遠(yuǎn)程操作權(quán)限,最后刪除子賬號(hào)及權(quán)限的完整流程,涵蓋賬號(hào)管理與權(quán)限配置關(guān)鍵步驟2025-10-10
mysql?dblink跨庫(kù)關(guān)聯(lián)查詢的實(shí)現(xiàn)
本文主要介紹了mysql?dblink跨庫(kù)關(guān)聯(lián)查詢的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
SQL刪除重復(fù)數(shù)據(jù)的實(shí)例教程
在使用SQL提數(shù)的時(shí)候,常會(huì)遇到表內(nèi)有重復(fù)值的時(shí)候,下面這篇文章主要給大家介紹了關(guān)于SQL刪除重復(fù)數(shù)據(jù)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
linux下mysql5.7.17最新穩(wěn)定版本安裝教程
這篇文章主要為大家詳細(xì)介紹了linux上mysql5.7.17最新穩(wěn)定版本安裝教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02
MySQL系列數(shù)據(jù)庫(kù)設(shè)計(jì)三范式教程示例
這篇文章主要為大家介紹了MySQL系列之?dāng)?shù)據(jù)庫(kù)設(shè)計(jì)三范式的教程示例講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-10-10
基于proxysql實(shí)現(xiàn)MySQL讀寫分離的實(shí)現(xiàn)實(shí)例
這篇文章主要介紹了基于proxysql實(shí)現(xiàn)MySQL讀寫分離,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-11-11

