之前接觸到的數據庫死鎖,都是批量更新時加鎖順序不一致而致使的死鎖,可是上週卻遇到了一個很難理解的死鎖。藉着這個機會又從新學習了一下mysql的死鎖知識以及常見的死鎖場景。在多方調研以及和同事們的討論下終於發現了這個死鎖問題的成因,收穫頗多。雖然是後端程序員,咱們不須要像DBA同樣深刻地去分析與鎖相關的源碼,可是若是咱們可以掌握基本的死鎖排查方法,對咱們的平常開發仍是大有裨益的。javascript
PS:本文不會介紹死鎖的基本知識,mysql的加鎖原理能夠參考本文的參考資料提供的連接。java
先介紹一下數據庫和表狀況,由於涉及到公司內部真是的數據,因此如下都作了模擬,不會影響具體的分析。mysql
咱們採用的是5.5版本的mysql數據庫,事務隔離級別是默認的RR(Repeatable-Read),採用innodb引擎。假設存在test表:程序員
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`a` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `a` (`a`)
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8;複製代碼
表的結構很簡單,一個主鍵id,另外一個惟一索引a。表裏的數據以下:sql
mysql> select * from test;
+----+------+
| id | a |
+----+------+
| 1 | 1 |
| 2 | 2 |
| 4 | 4 |
+----+------+
3 rows in set (0.00 sec)複製代碼
出現死鎖的操做以下:數據庫
步驟 | 事務1 | 事務2 |
---|---|---|
1 | begin | |
2 | delete from test where a = 2; | |
3 | begin | |
4 | delete from test where a = 2; (事務1卡住) | |
5 | 提示出現死鎖:ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | insert into test (id, a) values (10, 2); |
而後咱們能夠經過SHOW ENGINE INNODB STATUS;
來查看死鎖日誌:後端
------------------------
LATEST DETECTED DEADLOCK
------------------------
170219 13:31:31
*** (1) TRANSACTION:
TRANSACTION 2A8BD, ACTIVE 11 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 448218, OS thread handle 0x2abe5fb5d700, query id 18923238 renjun.fangcloud.net 121.41.41.92 root updating
delete from test where a = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BD lock_mode X waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
*** (2) TRANSACTION:
TRANSACTION 2A8BC, ACTIVE 18 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1248, 3 row lock(s), undo log entries 2
MySQL thread id 448217, OS thread handle 0x2abe5fd65700, query id 18923239 renjun.fangcloud.net 121.41.41.92 root update
insert into test (id,a) values (10,2)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BC lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BC lock mode S waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
*** WE ROLL BACK TRANSACTION (1)複製代碼
遇到死鎖,第一步就是閱讀死鎖日誌。死鎖日誌一般分爲兩部分,上半部分說明了事務1在等待什麼鎖:併發
170219 13:31:31
*** (1) TRANSACTION:
TRANSACTION 2A8BD, ACTIVE 11 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 448218, OS thread handle 0x2abe5fb5d700, query id 18923238 renjun.fangcloud.net 121.41.41.92 root updating
delete from test where a = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BD lock_mode X waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;複製代碼
從日誌裏咱們能夠看到事務1當前正在執行delete from test where a = 2
,該條語句正在申請索引a的X鎖,因此提示lock_mode X waiting
。高併發
而後日誌的下半部分說明了事務2當前持有的鎖以及等待的鎖:源碼分析
*** (2) TRANSACTION:
TRANSACTION 2A8BC, ACTIVE 18 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1248, 3 row lock(s), undo log entries 2
MySQL thread id 448217, OS thread handle 0x2abe5fd65700, query id 18923239 renjun.fangcloud.net 121.41.41.92 root update
insert into test (id,a) values (10,2)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BC lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BC lock mode S waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;複製代碼
從日誌的HOLDS THE LOCKS(S)
塊中咱們能夠看到事務2持有索引a的X鎖,而且是記錄鎖(Record Lock)。該鎖是經過事務2在步驟2執行的delete語句申請的。因爲是RR隔離模式下的基於惟一索引的等值查詢(Where a = 2),因此會申請一個記錄鎖,而非next-key鎖。
從日誌的WAITING FOR THIS LOCK TO BE GRANTED
塊中咱們能夠看到事務2正在申請S鎖,也就是共享鎖。該鎖是insert into test (id,a) values (10,2)
語句申請的。insert語句在普通狀況下是會申請排他鎖,也就是X鎖,可是這裏出現了S鎖。這是由於a字段是一個惟一索引,因此insert語句會在插入前進行一次duplicate key的檢查,爲了使此次檢查成功,須要申請S鎖防止其餘事務對a字段進行修改。
那麼爲何該S鎖會失敗呢?這是對同一個字段的鎖的申請是須要排隊的。S鎖前面還有一個未申請成功的X鎖,因此S鎖必須等待,因此造成了循環等待,死鎖出現了。
經過閱讀死鎖日誌,咱們能夠清楚地知道兩個事務造成了怎樣的循環等待,再加以分析,就能夠逆向推斷出循環等待的成因,也就是死鎖造成的緣由。
爲了讓你們更好地理解死鎖造成的緣由,咱們再經過表格的形式闡述死鎖造成的流程:
步驟 | 事務1 | 事務2 |
---|---|---|
1 | begin | |
2 | delete from test where a = 2; 執行成功,事務2佔有a=2下的X鎖,類型爲記錄鎖。 | |
3 | begin | |
4 | delete from test where a = 2; 事務1但願申請a=2下的X鎖,可是因爲事務2已經申請了一把X鎖,兩把X鎖互斥,因此X鎖申請進入鎖請求隊列。 | |
5 | 出現死鎖,事務1權重較小,因此被選擇回滾(成爲犧牲品)。 | insert into test (id, a) values (10, 2); 因爲a字段創建了惟一索引,因此須要申請S鎖以便檢查duplicate key,因爲插入的a的值仍是2,因此排在X鎖後面。可是前面的X鎖的申請只有在事務2commit或者rollback以後才能成功,此時造成了循環等待,死鎖產生。 |
在排查死鎖的過程當中,有個同事還發現了上述場景會產生另外一種死鎖,該場景沒法經過手工復現,只有高併發場景下才有可能復現。
該死鎖對應的日誌這裏就不貼出了,與上一個死鎖的核心差異是事務2等待的鎖從S鎖換成了X鎖,也就是lock_mode X locks gap before rec insert intention waiting
。咱們仍是經過表格來詳細說明該死鎖產生的流程:
步驟 | 事務1 | 事務2 |
---|---|---|
1 | begin | |
2 | delete from test where a = 2; 執行成功,事務2佔有a=2下的X鎖,類型爲記錄鎖。 | |
3 | begin | |
4 | 【insert第1階段】insert into test (id, a) values (10, 2); 事務2申請S鎖進行duplicate key進行檢查。檢查成功。 | |
5 | delete from test where a = 2; 事務1但願申請a=2下的X鎖,可是因爲事務2已經申請了一把X鎖,兩把X鎖互斥,因此X鎖申請進入鎖請求隊列。 | |
6 | 出現死鎖,事務1權重較小,因此被選擇回滾(成爲犧牲品)。 | 【insert第2階段】insert into test (id, a) values (10, 2); 事務2開始插入數據,S鎖升級爲X鎖,類型爲insert intention。同理,X鎖進入隊列排隊,造成循環等待,死鎖產生。 |
排查死鎖時,首先須要根據死鎖日誌分析循環等待的場景,而後根據當前各個事務執行的SQL分析出加鎖類型以及順序,逆向推斷出如何造成循環等待,這樣就能找到死鎖產生的緣由了。
PS:上述分析都是基於經驗的推斷,但願其餘小夥伴們可以指出當中的錯誤以及不足指出,謝謝!
本文首發於www.kissyu.org/2017/02/19/… 歡迎評論和轉載,轉載請註明來源,謝謝!