共計 8214 個字符,預計需要花費 21 分鐘才能閱讀完成。
今天丸趣 TV 小編給大家分享一下 mysql 出現死鎖的必要條件是什么的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
在 mysql 中,死鎖指的是在兩個或兩個以上不同的進程或線程中,因爭奪資源而造成的一種互相等待的現象;由于存在共同資源的競爭或進程(或線程)間的通訊而導致各個線程間相互掛起等待,如果沒有外力作用,最終會引發整個系統崩潰。mysql 出現死鎖的必要條件:1、資源獨占條件;2、請求和保持條件;3、不剝奪條件;4、相互獲取鎖條件。
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 程序操作的過程和時間線如下:
1.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;
3. 在事務 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 數據的寫鎖。
4. 在事務 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 數據的寫鎖。
5. 為什么會出現這種情況呢?
主要是因為事務 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、如何預防死鎖
阻止死鎖的途徑就是避免滿足死鎖條件的情況發生,為此我們在開發的過程中需要遵循如下原則:
1. 盡量避免并發的執行涉及到修改數據的語句。
2. 要求每一個事務一次就將所有要使用到的數據全部加鎖,否則就不允許執行。
3. 預先規定一個加鎖順序,所有的事務都必須按照這個順序對數據執行封鎖。如不同的過程在事務內部對對象的更新執行順序應盡量保證一致。
4. 每個事務的執行時間不可太長,對程序段的事務可考慮將其分割為幾個事務。在事務中不要求輸入,應該在事務之前得到輸入,然后快速執行事務。
5. 使用盡可能低的隔離級別。
6. 數據存儲空間離散法。該方法是指采用各種手段,將邏輯上在一個表中的數據分散的若干離散的空間上去,以便改善對表的訪問性能。主要通過將大表按行或者列分解為若干小表,或者按照不同的用戶群兩種方法實現。
7. 編寫應用程序,讓進程持有鎖的時間盡可能短,這樣其它進程就不必花太長的時間等待鎖被釋放。
死鎖的概念:
如果一組進程中的每一個進程都在等待僅由該組進程中的其他進程才能引發的事件,那么改組進程是死鎖的。
死鎖的常見表現:
死鎖不僅會發生多個進程中,也會發生在一個進程中。
(1)多進程死鎖:有進程 A,進程 B,進程 A 擁有資源 1,需要請求正在被進程 B 占有的資源 2。而進程 B 擁有資源 2,請求正在被進程 A 戰友的資源 1。兩個進程都在等待對方釋放資源后請求該資源,而相互僵持,陷入死鎖。
(2)單進程死鎖:進程 A 擁有資源 1,而它又在請求資源 1,而它所請求的資源 1 必須等待該資源使用完畢得到釋放后才可被請求。這樣,就陷入了自己的死鎖。
產生死鎖的原因:
(1)進程推進順序不當造成死鎖。
(2)競爭不可搶占性資源引起死鎖。
(3)競爭可消耗性資源引起死鎖。
死鎖的四個必要條件(四個條件四者不可缺一):
(1)互斥條件。某段時間內,一個資源一次只能被一個進程訪問。
(2)請求和保持條件。進程 A 已經擁有至少一個資源,此時又去申請其他資源,而該資源又正在被進程使用,此時請求進程阻塞,但對自己已經獲得的資源保持不放。
(3)不可搶占資源。進程已獲得的資源在未使用完不能被搶占,只能在自己使用完時由自己釋放。
(4)循環等待序列。存在一個循環等待序列 P0P1P2……Pn,P0 請求正在被進程 P1 占有的資源,P1 請求正在被 P2 占有的資源……Pn 正在請求被進程 P0 占有的資源。
解除死鎖的兩種方法:
(1)終止(或撤銷)進程。終止(或撤銷)系統中的一個或多個死鎖進程,直至打破循環環路,使系統從死鎖狀態中解除出來。
(2)搶占資源。從一個或多個進程中搶占足夠數量的資源,分配給死鎖進程,以打破死鎖狀態。
6、死鎖場景
本文死鎖場景皆為工作中遇到(或同事遇到)并解決的死鎖場景,寫這篇文章的目的是整理和分享,歡迎指正和補充,本文死鎖場景包括:
行鎖導致死鎖
gap lock/next keys lock 導致死鎖
index merge 導致死鎖
唯一索引沖突導致死鎖
注:以下場景隔離級別均為默認的 Repeatable Read;
1)行鎖導致死鎖
死鎖原因詳解:
1. 兩個事務執行過程時間上有交集,并且過程發生在兩者提交之前
2. 事務 1 更新 uid= 1 的記錄,事務 2 更新 uid= 2 的記錄,在 RR 級別,由于 uid 是唯一索引,因此兩個事務將分別持有 uid= 1 和 2 所在行的獨占鎖
3. 事務 1 執行到第二條更新語句時,發現 uid= 2 的行被鎖住,進入阻塞等待鎖釋放;
4. 事務 2 執行到第二條語句時發現 uid= 1 的行被鎖,同樣進入阻塞
5. 兩個事務互相等待,死鎖產生。
相應業務案例和解決方案:
該場景常見于事務中存在 for 循環更新某條記錄的情況,死鎖日志顯示 lock_mode X locks rec but not gap waiting(即行鎖而非間隙鎖),解決方案:
1. 避免循環更新,優化為一條 where 鎖定要更新的記錄批量更新
2. 如果非要循環更新,嘗試取消事務(能接受的話),即每一條更新為一個獨立的事務
2)gap lock/next keys lock 導致死鎖
死鎖原因分析:
1. 事務 1 執行 delete age = 27,務 2 執行 delete age = 31,在 RR 級別,操作條件不是唯一索引時,行鎖會升級為 next keys
lock(可以理解為間隙鎖),因此事務 1 鎖住了 25 到 27 和 27 到 29 的區間,事務 2 鎖住了 29 到 31 的區間
2. 事務 1 執行 insert age = 30,等待事務 2 釋放鎖
3. 事務 2 執行 insert age = 28,等待事務 1 釋放鎖
4. 死鎖產生,死鎖日志顯示 lock_mode X locks gap before rec insert intention waiting
解決方案:
1. 降低事務隔離級別到 Read Committed,該隔離級別下間隙鎖降級為行鎖,可以減少死鎖發生的概率
2. 避免這種場景 - –
3)index merge 導致死鎖
t_user 結構改造為:
死鎖分析:
1. 在符合場景前提的情況下(即表數據量較大,index_merge 未關閉),通過 explain 分析 update t_user where zone_id = 1 and uid = 1 可以發現 type 是 index_merge,即會用到 zone_id 和 uid 兩個索引
2. 上鎖的過程為:
事務 1:
① 鎖住 zone_id= 1 對應的間隙鎖: zoneId in (1,2)
② 鎖住索引 zone_id= 1 對應的主鍵索引行鎖 id = [1,2]
③ 鎖住 uid= 1 對應的間隙鎖: uid in (1, 2)
④ 鎖住 uid= 1 對應的主鍵索引行鎖: id = [1, 3]
事務 2:
① 鎖住 zone_id= 2 對應的間隙鎖: zoneId in (1,2)
② 鎖住索引 zone_id= 2 對應的主鍵索引行鎖 id = [3,4]
③ 鎖住 uid= 2 對應的間隙鎖: uid in (1,2)
④ 鎖住 uid= 2 對應的主鍵索引行鎖: id = [2, 4]
1、如果兩個事務上鎖的順序相反,則有一定的概率出現死鎖。另外,index_merge 的形式鎖住了很多不符合條件的行,浪費了資源。一般死鎖日志打印的信息為:
lock_mode
X locks rec but not gap waiting Record lock
解決方案:創建聯合索引,使執行計劃只會用到一個索引。
注:
update table set name = “wea” where col_1 = 1 or col_2 = 2 ;
col_1 和 col_2 為聯合索引,遵循最左原則 col_1 會走索引,但 col_2 會對整個索引進行掃描,此時會對整個索引加鎖。
以上就是“mysql 出現死鎖的必要條件是什么”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,丸趣 TV 小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注丸趣 TV 行業資訊頻道。
向 AI 問一下細節
丸趣 TV 網 – 提供最優質的資源集合!