MySQL死鎖排查與預(yù)防實(shí)戰(zhàn)
前言
線上日志里突然出現(xiàn)大量這個(gè)錯(cuò)誤:
Deadlock found when trying to get lock; try restarting transaction
死鎖是MySQL高并發(fā)場景下的常見問題。偶爾一兩次可以通過業(yè)務(wù)重試解決,但如果頻繁出現(xiàn),就需要從根本上排查和優(yōu)化。
這篇整理MySQL死鎖的排查方法和預(yù)防策略。
一、查看死鎖信息
MySQL有個(gè)命令能看到最近一次死鎖的詳情:
SHOW ENGINE INNODB STATUS\G
輸出很長,找LATEST DETECTED DEADLOCK這部分:
*** (1) TRANSACTION: UPDATE orders SET status = 'paid' WHERE id = 1001 *** (1) HOLDS THE LOCK(S): -- 持有orders表的鎖 *** (1) WAITING FOR THIS LOCK: -- 等inventory表的鎖 *** (2) TRANSACTION: UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 2001 *** (2) HOLDS THE LOCK(S): -- 持有inventory表的鎖 *** (2) WAITING FOR THIS LOCK: -- 等orders表的鎖 *** WE ROLL BACK TRANSACTION (2)
經(jīng)典的死鎖場景:事務(wù)A鎖了orders等inventory,事務(wù)B鎖了inventory等orders,互相等。
二、分析死鎖原因
知道是哪兩個(gè)SQL了,回去翻代碼。
原來下單邏輯里有兩種調(diào)用順序:
// 路徑A:先改訂單再扣庫存 updateOrderStatus(orderId, "paid"); decreaseInventory(productId, 1); // 路徑B:先扣庫存再改訂單(另一個(gè)接口) decreaseInventory(productId, 1); updateOrderStatus(orderId, "paid");
兩個(gè)接口都在事務(wù)里,剛好并發(fā)了就死鎖。
三、解決方案
最直接的辦法:統(tǒng)一加鎖順序。
不管哪個(gè)接口,都先操作orders再操作inventory(或者反過來,總之要一致)。
// 統(tǒng)一順序:先orders后inventory
@Transactional
public void processOrder(long orderId, long productId) {
updateOrderStatus(orderId, "paid"); // 永遠(yuǎn)先鎖orders
decreaseInventory(productId, 1); // 再鎖inventory
}
如果涉及多條記錄,按ID排序:
List<Long> ids = Arrays.asList(id1, id2, id3);
Collections.sort(ids);
for (Long id : ids) {
lockAndProcess(id);
}
四、間隙鎖導(dǎo)致的死鎖
還有一種更詭異的死鎖,兩個(gè)事務(wù)操作的都不是同一行數(shù)據(jù)。
這通常是間隙鎖的問題。RR隔離級(jí)別下,SELECT ... FOR UPDATE如果沒命中數(shù)據(jù),會(huì)鎖一個(gè)"間隙"。
比如user_id有1、5、10三條記錄:
-- 事務(wù)A SELECT * FROM orders WHERE user_id = 3 FOR UPDATE; -- 沒有user_id=3的數(shù)據(jù),但會(huì)鎖住(1,5)這個(gè)間隙 -- 事務(wù)B SELECT * FROM orders WHERE user_id = 7 FOR UPDATE; -- 鎖住(5,10)這個(gè)間隙 -- 然后兩邊各自INSERT -- 事務(wù)A想插入user_id=6,要等(5,10)的間隙鎖 -- 事務(wù)B想插入user_id=4,要等(1,5)的間隙鎖 -- 死鎖
解決辦法:
- 改用RC隔離級(jí)別(間隙鎖少很多,但要注意幻讀)
- 用唯一索引精確查詢,避免范圍鎖
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
五、縮小事務(wù)范圍
還有個(gè)常見問題是事務(wù)太長。事務(wù)越長,持有鎖的時(shí)間越久,死鎖概率越高。
// 這種寫法不好
@Transactional
public void process() {
queryData(); // 查數(shù)據(jù)
callExternalApi(); // 調(diào)外部接口,可能很慢
updateDatabase(); // 更新數(shù)據(jù)庫
}
// 改成這樣
public void process() {
queryData();
callExternalApi(); // 外部調(diào)用放事務(wù)外面
updateInTransaction();
}
@Transactional
public void updateInTransaction() {
updateDatabase(); // 只有真正需要事務(wù)的操作
}
六、監(jiān)控與告警
建議加上監(jiān)控:
# 簡單腳本,每分鐘檢查死鎖次數(shù)
DEADLOCKS=$(mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks'" | awk 'NR==2{print $2}')
echo "$(date) deadlocks: $DEADLOCKS" >> /var/log/deadlock.log
配合Prometheus的話:
- alert: MySQLDeadlock expr: increase(mysql_global_status_innodb_deadlocks[5m]) > 0 for: 1m
死鎖次數(shù)漲了就告警,別等業(yè)務(wù)反饋才知道。
七、業(yè)務(wù)層重試
有些場景死鎖確實(shí)很難完全避免,那就在業(yè)務(wù)層做重試:
int retry = 3;
while (retry-- > 0) {
try {
doTransaction();
break;
} catch (DeadlockException e) {
if (retry == 0) throw e;
Thread.sleep(100); // 等一下再試
}
}
MySQL檢測到死鎖會(huì)立即回滾一個(gè)事務(wù),不會(huì)一直卡著,所以重試通常能成功。
總結(jié)
死鎖本質(zhì)是資源競爭問題,預(yù)防比解決更重要:
| 方法 | 效果 |
|---|---|
| 統(tǒng)一加鎖順序 | 最有效,從根本上避免死鎖 |
| 縮小事務(wù)范圍 | 減少鎖持有時(shí)間 |
| 合理使用索引 | 減少鎖的范圍 |
| 降低隔離級(jí)別 | 減少間隙鎖(RC級(jí)別) |
| 業(yè)務(wù)層重試 | 兜底方案 |
記住兩點(diǎn):統(tǒng)一加鎖順序、縮小事務(wù)范圍,能解決大部分死鎖問題。
到此這篇關(guān)于MySQL死鎖排查與預(yù)防實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)MySQL死鎖排查內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用MySQL函數(shù)實(shí)現(xiàn)判斷視頻擴(kuò)展名的代碼
MySQL擁有強(qiáng)大的自定義函數(shù)功能,如下,我寫了一個(gè)用MySQL函數(shù) 判斷視頻地址是否可以手機(jī)端播放2012-02-02
MySQL多實(shí)例管理如何在一臺(tái)主機(jī)上運(yùn)行多個(gè)mysql
文章詳解了在Linux主機(jī)上通過二進(jìn)制方式安裝MySQL多實(shí)例的步驟,涵蓋端口配置、數(shù)據(jù)目錄準(zhǔn)備、初始化與啟動(dòng)流程,以及排錯(cuò)方法,適用于構(gòu)建讀寫分離架構(gòu),感興趣的朋友一起看看吧2025-07-07
mysql數(shù)據(jù)庫提權(quán)的三種方法
文介紹了MySQL數(shù)據(jù)庫的三種提權(quán)方法:UDF提權(quán)、MOF提權(quán)和啟動(dòng)項(xiàng)提權(quán),同時(shí)列出了一些常見數(shù)據(jù)庫及其默認(rèn)端口,下面就來介紹一下,感興趣的可以了解一下2024-09-09
MySQL中使用PROFILING來查看SQL執(zhí)行流程的實(shí)現(xiàn)步驟
在MySQL中,PROFILING功能提供了一種方式來分析SQL語句的執(zhí)行時(shí)間,包括查詢執(zhí)行的各個(gè)階段,如發(fā)送、解析、優(yōu)化、執(zhí)行等,這對于診斷性能問題非常有用,本文給大家介紹了MySQL中使用PROFILING來查看SQL執(zhí)行流程的實(shí)現(xiàn)步驟,需要的朋友可以參考下2024-07-07
MySQL多表關(guān)聯(lián)查詢方式及實(shí)際應(yīng)用
MySQL語句學(xué)習(xí)的難點(diǎn)和重點(diǎn)就在于多表查詢,同時(shí)MySQL也有諸多方法供大家選擇,不論是多表聯(lián)查(聯(lián)結(jié)表、左連接、右連接……),這篇文章主要給大家介紹了關(guān)于MySQL多表關(guān)聯(lián)查詢方式及實(shí)際應(yīng)用的相關(guān)資料,需要的朋友可以參考下2024-07-07

