阿里一面 | 說說你對 MySQL 死鎖的理解
點擊下方“IT牧場”,選擇“設為星標”

◆?1、什么是死鎖?
死鎖指的是在兩個或兩個以上不同的進程或線程中,由于存在共同資源的競爭或進程(或線程)間的通訊而導致各個線程間相互掛起等待,如果沒有外力作用,最終會引發(fā)整個系統(tǒng)崩潰。
◆?2、Mysql出現(xiàn)死鎖的必要條件
資源獨占條件
指多個事務在競爭同一個資源時存在互斥性,即在一段時間內(nèi)某資源只由一個事務占用,也可叫獨占資源(如行鎖)。
請求和保持條件
指在一個事務a中已經(jīng)獲得鎖A,但又提出了新的鎖B請求,而該鎖B已被其它事務b占有,此時該事務a則會阻塞,但又對自己已獲得的鎖A保持不放。
不剝奪條件
指一個事務a中已經(jīng)獲得鎖A,在未提交之前,不能被剝奪,只能在使用完后提交事務再自己釋放。
相互獲取鎖條件
指在發(fā)生死鎖時,必然存在一個相互獲取鎖過程,即持有鎖A的事務a在獲取鎖B的同時,持有鎖B的事務b也在獲取鎖A,最終導致相互獲取而各個事務都阻塞。
◆?3、 Mysql經(jīng)典死鎖案例
假設存在一個轉(zhuǎn)賬情景,A賬戶給B賬戶轉(zhuǎn)賬50元的同時,B賬戶也給A賬戶轉(zhuǎn)賬30元,那么在這過程中是否會存在死鎖情況呢?
◆?3.1 建表語句
CREATE TABLE `account` (
`id` int(11) NOT NULL COMMENT '主鍵',
`user_id` varchar(56) NOT NULL COMMENT '用戶id',
`balance` float(10,2) DEFAULT NULL COMMENT '余額',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='賬戶余額表';
◆?3.2 初始化相關(guān)數(shù)據(jù)
INSERT INTO `test`.`account` (`id`, `user_id`, `balance`) VALUES (1, 'A', 80.00);
INSERT INTO `test`.`account` (`id`, `user_id`, `balance`) VALUES (2, 'B', 60.00);

◆?3.3 正常轉(zhuǎn)賬過程
在說死鎖問題之前,咱們先來看看正常的轉(zhuǎn)賬過程。
正常情況下,A用戶給B用戶轉(zhuǎn)賬50元,可在一個事務內(nèi)完成,需要先獲取A用戶的余額和B用戶的余額,因為之后需要修改這兩條數(shù)據(jù),所以需要通過寫鎖(for UPDATE)鎖住他們,防止其他事務更改導致我們的更改丟失而引起臟數(shù)據(jù)。
相關(guān)sql如下:
開啟事務之前需要先把mysql的自動提交關(guān)閉
set autocommit=0;
# 查看事務自動提交狀態(tài)狀態(tài)
show VARIABLES like 'autocommit';
# 轉(zhuǎn)賬sql
START TRANSACTION;
# 獲取A 的余額并存入A_balance變量:80
SELECT user_id,@A_balance:=balance from account where user_id = 'A' for UPDATE;
# 獲取B 的余額并存入B_balance變量:60
SELECT user_id,@B_balance:=balance from account where user_id = 'B' for UPDATE;
# 修改A 的余額
UPDATE account set balance = @A_balance - 50 where user_id = 'A';
# 修改B 的余額
UPDATE account set balance = @B_balance + 50 where user_id = 'B';
COMMIT;
執(zhí)行后的結(jié)果:

可以看到數(shù)據(jù)更新都是正常的情況
◆?3.4 死鎖轉(zhuǎn)賬過程
初始化的余額為:

假設在高并發(fā)情況下存在這種場景,A用戶給B用戶轉(zhuǎn)賬50元的同時,B用戶也給A用戶轉(zhuǎn)賬30元。
那么我們的java程序操作的過程和時間線如下:
A用戶給B用戶轉(zhuǎn)賬50元,需在程序中開啟事務1來執(zhí)行sql,并獲取A的余額同時鎖住A這條數(shù)據(jù)。
# 事務1
set autocommit=0;
START TRANSACTION;
# 獲取A 的余額并存入A_balance變量:80
SELECT user_id,@A_balance:=balance from account where user_id = 'A' for UPDATE;
2.B用戶給A用戶轉(zhuǎn)賬30元,需在程序中開啟事務2來執(zhí)行sql,并獲取B的余額同時鎖住B這條數(shù)據(jù)。
# 事務2
set autocommit=0;
START TRANSACTION;
# 獲取A 的余額并存入A_balance變量:60
SELECT user_id,@A_balance:=balance from account where user_id = 'B' for UPDATE;
3.在事務1中執(zhí)行剩下的sql
# 獲取B 的余額并存入B_balance變量:60
SELECT user_id,@B_balance:=balance from account where user_id = 'B' for UPDATE;
# 修改A 的余額
UPDATE account set balance = @A_balance - 50 where user_id = 'A';
# 修改B 的余額
UPDATE account set balance = @B_balance + 50 where user_id = 'B';
COMMIT;

可以看到,在事務1中獲取B數(shù)據(jù)的寫鎖時出現(xiàn)了超時情況。為什么會這樣呢?主要是因為我們在步驟2的時候已經(jīng)在事務2中獲取到B數(shù)據(jù)的寫鎖了,那么在事務2提交或回滾前事務1永遠都拿不到B數(shù)據(jù)的寫鎖。
4.在事務2中執(zhí)行剩下的sql
# 獲取A 的余額并存入B_balance變量:60
SELECT user_id,@B_balance:=balance from account where user_id = 'A' for UPDATE;
# 修改B 的余額
UPDATE account set balance = @A_balance - 30 where user_id = 'B';
# 修改A 的余額
UPDATE account set balance = @B_balance + 30 where user_id = 'A';
COMMIT;

同理可得,在事務2中獲取A數(shù)據(jù)的寫鎖時也出現(xiàn)了超時情況。因為步驟1的時候已經(jīng)在事務1中獲取到A數(shù)據(jù)的寫鎖了,那么在事務1提交或回滾前事務2永遠都拿不到A數(shù)據(jù)的寫鎖。
5. 為什么會出現(xiàn)這種情況呢?
主要是因為事務1和事務2存在相互等待獲取鎖的過程,導致兩個事務都掛起阻塞,最終拋出獲取鎖超時的異常。

◆?3.5 死鎖導致的問題
眾所周知,數(shù)據(jù)庫的連接資源是很珍貴的,如果一個連接因為事務阻塞長時間不釋放,那么后面新的請求要執(zhí)行的sql也會排隊等待,越積越多,最終會拖垮整個應用。一旦你的應用部署在微服務體系中而又沒有做熔斷處理,由于整個鏈路被阻斷,那么就會引發(fā)雪崩效應,導致很嚴重的生產(chǎn)事故。
◆?4、如何解決死鎖問題?
要想解決死鎖問題,我們可以從死鎖的四個必要條件入手。
由于資源獨占條件和不剝奪條件是鎖本質(zhì)的功能體現(xiàn),無法修改,所以咱們從另外兩個條件嘗試去解決。
◆?4.1 打破請求和保持條件
根據(jù)上面定義可知,出現(xiàn)這個情況是因為事務1和事務2同時去競爭鎖A和鎖B,那么我們是否可以保證鎖A和鎖B一次只能被一個事務競爭和持有呢?
答案是肯定可以的。下面咱們通過偽代碼來看看:
/**
* 事務1入?yún)?A, B)
* 事務2入?yún)?B, A)
**/
public void transferAccounts(String userFrom, String userTo) {
// 獲取分布式鎖
Lock lock = Redisson.getLock();
// 開啟事務
JDBC.excute("START TRANSACTION;");
// 執(zhí)行轉(zhuǎn)賬sql
JDBC.excute("# 獲取A 的余額并存入A_balance變量:80\n" +
"SELECT user_id,@A_balance:=balance from account where user_id = '" + userFrom + "' for UPDATE;\n" +
"# 獲取B 的余額并存入B_balance變量:60\n" +
"SELECT user_id,@B_balance:=balance from account where user_id = '" + userTo + "' for UPDATE;\n" +
"\n" +
"# 修改A 的余額\n" +
"UPDATE account set balance = @A_balance - 50 where user_id = '" + userFrom + "';\n" +
"# 修改B 的余額\n" +
"UPDATE account set balance = @B_balance + 50 where user_id = '" + userTo + "';\n");
// 提交事務
JDBC.excute("COMMIT;");
// 釋放鎖
lock.unLock();
}
上面的偽代碼顯而易見可以解決死鎖問題,因為所有的事務都是通過分布式鎖來串行執(zhí)行的。
那么這樣就真的萬事大吉了嗎?
在小流量情況下看起來是沒問題的,但是在高并發(fā)場景下這里將成為整個服務的性能瓶頸,因為即使你部署了再多的機器,但由于分布式鎖的原因,你的業(yè)務也只能串行進行,服務性能并不因為集群部署而提高并發(fā)量,完全無法滿足分布式業(yè)務下快、準、穩(wěn)的要求,所以咱們不妨換種方式來看看怎么解決死鎖問題。
◆?4.2 打破相互獲取鎖條件(推薦)
要打破這個條件其實也很簡單,那就是事務再獲取鎖的過程中保證順序獲取即可,也就是鎖A始終在鎖B之前獲取。
我們來看看之前的偽代碼怎么優(yōu)化?
/**
* 事務1入?yún)?A, B)
* 事務2入?yún)?B, A)
**/
public void transferAccounts(String userFrom, String userTo) {
// 對用戶A和B進行排序,讓userFrom始終為用戶A,userTo始終為用戶B
if (userFrom.hashCode() > userTo.hashCode()) {
String tmp = userFrom;
userFrom = userTo;
userTo = tmp;
}
// 開啟事務
JDBC.excute("START TRANSACTION;");
// 執(zhí)行轉(zhuǎn)賬sql
JDBC.excute("# 獲取A 的余額并存入A_balance變量:80\n" +
"SELECT user_id,@A_balance:=balance from account where user_id = '" + userFrom + "' for UPDATE;\n" +
"# 獲取B 的余額并存入B_balance變量:60\n" +
"SELECT user_id,@B_balance:=balance from account where user_id = '" + userTo + "' for UPDATE;\n" +
"\n" +
"# 修改A 的余額\n" +
"UPDATE account set balance = @A_balance - 50 where user_id = '" + userFrom + "';\n" +
"# 修改B 的余額\n" +
"UPDATE account set balance = @B_balance + 50 where user_id = '" + userTo + "';\n");
// 提交事務
JDBC.excute("COMMIT;");
}
假設事務1的入?yún)?A, B),事務2入?yún)?B, A),由于我們對兩個用戶參數(shù)進行了排序,所以在事務1中需要先獲取鎖A在獲取鎖B,事務2也是一樣要先獲取鎖A在獲取鎖B,兩個事務都是順序獲取鎖,所以也就打破了相互獲取鎖的條件,最終完美解決死鎖問題。
◆?5、總結(jié)
因為mysql在互聯(lián)網(wǎng)中的大量使用,所以死鎖問題還是經(jīng)常會被問到,希望兄弟們能掌握這方面的知識,提高自己的競爭力。
來源:
https://www.cnblogs.com/yin-feng/p/16041014.html
干貨分享
最近將個人學習筆記整理成冊,使用PDF分享。關(guān)注我,回復如下代碼,即可獲得百度盤地址,無套路領(lǐng)取!
?001:《Java并發(fā)與高并發(fā)解決方案》學習筆記;?002:《深入JVM內(nèi)核——原理、診斷與優(yōu)化》學習筆記;?003:《Java面試寶典》?004:《Docker開源書》?005:《Kubernetes開源書》?006:《DDD速成(領(lǐng)域驅(qū)動設計速成)》?007:全部?008:加技術(shù)群討論
加個關(guān)注不迷路
喜歡就點個"在看"唄^_^
