記錄一次 Mysql 死鎖排查過程

背景

之前接觸到的數據庫死鎖,都是批量更新時加鎖順序不一致而致使的死鎖,可是上週卻遇到了一個很難理解的死鎖。藉着這個機會又從新學習了一下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/… 歡迎評論和轉載,轉載請註明來源,謝謝!

相關文章
相關標籤/搜索