阿里一面,說說你對 Mysql 死鎖的理解
1、什麼是死鎖?
死鎖指的是在兩個或兩個以上不同的進程或線程中,由於存在共同資源的競爭或進程(或線程)間的通訊而導致各個線程間相互掛起等待,如果沒有外力作用,最終會引發整個系統崩潰。
2、Mysql 出現死鎖的必要條件
- 資源獨佔條件
指多個事務在競爭同一個資源時存在互斥性,即在一段時間內某資源只由一個事務佔用,也可叫獨佔資源(如行鎖)。
- 請求和保持條件
指在一個事務 a 中已經獲得鎖 A,但又提出了新的鎖 B 請求,而該鎖 B 已被其它事務 b 佔有,此時該事務 a 則會阻塞,但又對自己已獲得的鎖 A 保持不放。
- 不剝奪條件
指一個事務 a 中已經獲得鎖 A,在未提交之前,不能被剝奪,只能在使用完後提交事務再自己釋放。
- 相互獲取鎖條件
指在發生死鎖時,必然存在一個相互獲取鎖過程,即持有鎖 A 的事務 a 在獲取鎖 B 的同時,持有鎖 B 的事務 b 也在獲取鎖 A,最終導致相互獲取而各個事務都阻塞。
3、 Mysql 經典死鎖案例
假設存在一個轉賬情景,A 賬戶給 B 賬戶轉賬 50 元的同時,B 賬戶也給 A 賬戶轉賬 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 初始化相關數據
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 正常轉賬過程
在說死鎖問題之前,咱們先來看看正常的轉賬過程。
正常情況下,A 用戶給 B 用戶轉賬 50 元,可在一個事務內完成,需要先獲取 A 用戶的餘額和 B 用戶的餘額,因爲之後需要修改這兩條數據,所以需要通過寫鎖(for UPDATE)鎖住他們,防止其他事務更改導致我們的更改丟失而引起髒數據。
相關 sql 如下:
開啓事務之前需要先把 mysql 的自動提交關閉
set autocommit=0;
# 查看事務自動提交狀態狀態
show VARIABLES like 'autocommit';
# 轉賬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;
執行後的結果:
可以看到數據更新都是正常的情況
3.4 死鎖轉賬過程
初始化的餘額爲:
假設在高併發情況下存在這種場景,A 用戶給 B 用戶轉賬 50 元的同時,B 用戶也給 A 用戶轉賬 30 元。
那麼我們的 java 程序操作的過程和時間線如下:
- A 用戶給 B 用戶轉賬 50 元,需在程序中開啓事務 1 來執行 sql,並獲取 A 的餘額同時鎖住 A 這條數據。
# 事務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 用戶轉賬 30 元,需在程序中開啓事務 2 來執行 sql,並獲取 B 的餘額同時鎖住 B 這條數據。
# 事務2
set autocommit=0;
START TRANSACTION;
# 獲取A 的餘額並存入A_balance變量:60
SELECT user_id,@A_balance:=balance from account where user_id = 'B' for UPDATE;
- 在事務 1 中執行剩下的 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 數據的寫鎖時出現了超時情況。爲什麼會這樣呢?主要是因爲我們在步驟 2 的時候已經在事務 2 中獲取到 B 數據的寫鎖了,那麼在事務 2 提交或回滾前事務 1 永遠都拿不到 B 數據的寫鎖。
- 在事務 2 中執行剩下的 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 數據的寫鎖時也出現了超時情況。因爲步驟 1 的時候已經在事務 1 中獲取到 A 數據的寫鎖了,那麼在事務 1 提交或回滾前事務 2 永遠都拿不到 A 數據的寫鎖。
- 爲什麼會出現這種情況呢?
主要是因爲事務 1 和事務 2 存在相互等待獲取鎖的過程,導致兩個事務都掛起阻塞,最終拋出獲取鎖超時的異常。
3.5 死鎖導致的問題
衆所周知,數據庫的連接資源是很珍貴的,如果一個連接因爲事務阻塞長時間不釋放,那麼後面新的請求要執行的 sql 也會排隊等待,越積越多,最終會拖垮整個應用。一旦你的應用部署在微服務體系中而又沒有做熔斷處理,由於整個鏈路被阻斷,那麼就會引發雪崩效應,導致很嚴重的生產事故。
4、如何解決死鎖問題?
要想解決死鎖問題,我們可以從死鎖的四個必要條件入手。
由於資源獨佔條件和不剝奪條件是鎖本質的功能體現,無法修改,所以咱們從另外兩個條件嘗試去解決。
4.1 打破請求和保持條件
根據上面定義可知,出現這個情況是因爲事務 1 和事務 2 同時去競爭鎖 A 和鎖 B,那麼我們是否可以保證鎖 A 和鎖 B 一次只能被一個事務競爭和持有呢?
答案是肯定可以的。下面咱們通過僞代碼來看看:
/**
* 事務1入參(A, B)
* 事務2入參(B, A)
**/
public void transferAccounts(String userFrom, String userTo) {
// 獲取分佈式鎖
Lock lock = Redisson.getLock();
// 開啓事務
JDBC.excute("START TRANSACTION;");
// 執行轉賬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();
}
上面的僞代碼顯而易見可以解決死鎖問題,因爲所有的事務都是通過分佈式鎖來串行執行的。
那麼這樣就真的萬事大吉了嗎?
在小流量情況下看起來是沒問題的,但是在高併發場景下這裏將成爲整個服務的性能瓶頸,因爲即使你部署了再多的機器,但由於分佈式鎖的原因,你的業務也只能串行進行,服務性能並不因爲集羣部署而提高併發量,完全無法滿足分佈式業務下快、準、穩的要求,所以咱們不妨換種方式來看看怎麼解決死鎖問題。
4.2 打破相互獲取鎖條件(推薦)
要打破這個條件其實也很簡單,那就是事務再獲取鎖的過程中保證順序獲取即可,也就是鎖 A 始終在鎖 B 之前獲取。
我們來看看之前的僞代碼怎麼優化?
/**
* 事務1入參(A, B)
* 事務2入參(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;");
// 執行轉賬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 的入參爲 (A, B),事務 2 入參爲 (B, A),由於我們對兩個用戶參數進行了排序,所以在事務 1 中需要先獲取鎖 A 在獲取鎖 B,事務 2 也是一樣要先獲取鎖 A 在獲取鎖 B,兩個事務都是順序獲取鎖,所以也就打破了相互獲取鎖的條件,最終完美解決死鎖問題。
5、總結
因爲 mysql 在互聯網中的大量使用,所以死鎖問題還是經常會被問到,希望兄弟們能掌握這方面的知識,提高自己的競爭力。
來源:
https://www.cnblogs.com/yin-feng/p/16041014.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qwTDMsQBhVJY_3MpF2FFOA