MySQL死鎖排查指南
MySQL死鎖排查指南
作為一名10年經(jīng)驗的Java工程師,我會從場景、排查、解決三個維度,帶你搞定MySQL死鎖問題。
一、先搞懂:死鎖是什么?
死鎖是多個事務(wù)互相持有對方需要的資源,陷入無限等待的僵局。
它必須同時滿足4個“缺一不可”的條件(破壞任意一個就能避免死鎖):
- 資源獨占:一個資源(如一行數(shù)據(jù))只能被一個事務(wù)持有;
- 請求并持有:事務(wù)持有資源的同時,又請求其他資源且不釋放已有資源;
- 不可剝奪:事務(wù)已獲得的資源不能被強行搶占;
- 循環(huán)等待:事務(wù)之間形成“事務(wù)A等B,B等A”的閉環(huán)。
二、經(jīng)典場景:Java業(yè)務(wù)里的死鎖長啥樣?
以用戶互轉(zhuǎn)余額為例(Java+MySQL事務(wù)):
// 事務(wù)A:用戶A給B轉(zhuǎn)賬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);
}
// 事務(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的賬戶(此時A已被事務(wù)A鎖定,陷入等待)
accountMapper.updateBalance(aId, +amount);
}當兩個事務(wù)同時執(zhí)行時:
- 事務(wù)A持有A的鎖,等待B的鎖;
- 事務(wù)B持有B的鎖,等待A的鎖;
→ 死鎖產(chǎn)生。
三、死鎖排查:核心步驟+命令
當業(yè)務(wù)出現(xiàn)“接口超時、事務(wù)卡住”時,優(yōu)先排查死鎖。
步驟1:查看死鎖日志
MySQL(InnoDB引擎)最核心的排查命令:
SHOW ENGINE INNODB STATUS;
執(zhí)行后,找到 LATEST DETECTED DEADLOCK 模塊,關(guān)鍵信息包括:
TRANSACTION (1)/(2):沖突的兩個事務(wù);WAITING FOR THIS LOCK:事務(wù)等待的鎖及對應(yīng)的SQL;HOLDS THE LOCK(S):事務(wù)持有的鎖及對應(yīng)的SQL;WE ROLL BACK TRANSACTION (X):MySQL自動回滾的事務(wù)(解決死鎖)。
步驟2:結(jié)合Java業(yè)務(wù)定位代碼
根據(jù)死鎖日志里的SQL語句,找到對應(yīng)的Java代碼(比如上述transferAtoB方法),分析事務(wù)的加鎖順序是否不一致。
四、根治死鎖:Java業(yè)務(wù)里的落地方案
針對Java業(yè)務(wù),從代碼、數(shù)據(jù)庫兩個層面解決:
方案1:約定統(tǒng)一的加鎖順序(最有效)
我們約定一個全局規(guī)則:無論轉(zhuǎn)賬方向如何,都先鎖定 ID 字典序更小的賬戶,再鎖定 ID 更大的賬戶,這就是 “統(tǒng)一的加鎖順序”:
@Service
public class TransferService {
@Autowired
private AccountMapper accountMapper;
// 統(tǒng)一的轉(zhuǎn)賬方法(無論誰轉(zhuǎn)誰,都按ID大小順序加鎖)
@Transactional
public void transfer(String fromId, String toId, int amount) {
// 步驟1:確定加鎖順序(全局統(tǒng)一規(guī)則)
String lockFirstId; // 先鎖這個ID
String lockSecondId; // 后鎖這個ID
if (fromId.compareTo(toId) < 0) {
lockFirstId = fromId;
lockSecondId = toId;
} else {
lockFirstId = toId;
lockSecondId = fromId;
}
// 步驟2:按統(tǒng)一順序加鎖(先鎖小ID,再鎖大ID)
// 先鎖定第一個賬戶(無論它是轉(zhuǎn)出方還是轉(zhuǎn)入方)
if (lockFirstId.equals(fromId)) {
accountMapper.deductBalance(lockFirstId, amount); // 轉(zhuǎn)出
} else {
accountMapper.addBalance(lockFirstId, amount); // 轉(zhuǎn)入
}
// 再鎖定第二個賬戶
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)。
- 當調(diào)用transfer(“user_001”, “user_002”, 10)(A 轉(zhuǎn) B):先鎖user_001,再鎖user_002;
- 當調(diào)用transfer(“user_002”, “user_001”, 20)(B 轉(zhuǎn) A):依然先鎖user_001,再鎖user_002;
兩個事務(wù)的加鎖順序完全一致,不會出現(xiàn) “你等我、我等你” 的循環(huán)等待,從根源上杜絕死鎖。
流程展示
- 用戶A:ID為
user_001 - 用戶B:ID為
user_002 - 規(guī)則:
user_001的字典序 <user_002
無統(tǒng)一加鎖順序 → 死鎖(執(zhí)行流程)
當兩個事務(wù)各自按“轉(zhuǎn)出方→轉(zhuǎn)入方”的順序加鎖時:
| 時間線 | 事務(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)一加鎖順序 → 無死鎖(執(zhí)行流程)
當兩個事務(wù)都按“ID從小到大”的順序加鎖時:
| 時間線 | 事務(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提交,無死鎖 |
這樣是不是更清楚了?需要我把這個流程做成更簡潔的對比表格方便你保存嗎?
方案2:縮短事務(wù)范圍
避免事務(wù)中包含非數(shù)據(jù)庫操作(如RPC調(diào)用、日志打?。瑴p少鎖的持有時間:
// 壞例子:事務(wù)包含RPC調(diào)用(加長鎖持有時間)
@Transactional
public void badTransfer(String fromId, String toId, int amount) {
accountMapper.updateBalance(fromId, -amount);
rpcClient.notifyThirdParty(fromId, toId, amount); // 非DB操作,加長事務(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ù)庫層面(按需)
- 加索引:確保更新/查詢的
WHERE條件走索引,減少鎖的范圍(避免表鎖); - 降低隔離級別:業(yè)務(wù)允許的話,將隔離級別從
REPEATABLE-READ(默認)降為READ-COMMITTED,減少間隙鎖; - 顯式加鎖優(yōu)化:使用
SELECT ... FOR UPDATE顯式加鎖時,確保WHERE條件走索引。
到此這篇關(guān)于MySQL死鎖排查指南的文章就介紹到這了,更多相關(guān)mysql死鎖排查內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MySQL遇到“?Access?denied?for?user?”問題的解決辦法
這篇文章主要介紹了MySQL遇到“?Access?denied?for?user?”問題的解決辦法,文中通過代碼示例講解的非常詳細,對大家的解決問題有一定的幫助,需要的朋友可以參考下2024-12-12
MySQL中通過SQL語句刪除重復(fù)記錄并且只保留一條記錄
本文主要介紹了MySQL中通過SQL語句刪除重復(fù)記錄并且只保留一條記錄,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
MySQL 批量插入的原理和實戰(zhàn)方法(快速提升大數(shù)據(jù)導(dǎo)入效率)
在日常開發(fā)中,我們經(jīng)常需要將大量數(shù)據(jù)批量插入到 MySQL 數(shù)據(jù)庫中,本文將介紹批量插入的原理、實現(xiàn)方法,并結(jié)合 Python 和 PyMySQL 庫提供詳細的實戰(zhàn)示例,感興趣的朋友跟隨小編一起看看吧2025-11-11

