記錄一次Mysql死鎖排查過程

背景

之前接觸到的數據庫死鎖,都是批量更新時加鎖順序不一致而致使的死鎖,可是上週卻遇到了一個很難理解的死鎖。藉着這個機會又從新學習了一下mysql的死鎖知識以及常見的死鎖場景。在多方調研以及和同事們的討論下終於發現了這個死鎖問題的成因,收穫頗多。雖然是後端程序員,咱們不須要像DBA同樣深刻地去分析與鎖相關的源碼,可是若是咱們可以掌握基本的死鎖排查方法,對咱們的平常開發仍是大有裨益的。mysql

死鎖原由

先介紹一下數據庫和表狀況,由於涉及到公司內部真實的數據,因此如下都作了模擬,不會影響具體的分析。程序員

咱們採用的是5.5版本的mysql數據庫,事務隔離級別是默認的RR(Repeatable-Read),採用innodb引擎。假設存在test表:sql

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。表裏的數據以下:數據庫

mysql> select * from test;
+----+------+
| id | a    |
+----+------+
|  1 |    1 |
|  2 |    2 |
|  4 |    4 |
+----+------+
3 rows in set (0.00 sec)

出現死鎖的操做以下:後端

而後咱們能夠經過SHOW ENGINE INNODB STATUS;來查看死鎖日誌:架構

------------------------
LATEST DETECTED DEADLOCK
------------------------
190219 13:35: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在等待什麼鎖:併發

190219 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鎖必須等待,因此造成了循環等待,死鎖出現了。

經過閱讀死鎖日誌,咱們能夠清楚地知道兩個事務造成了怎樣的循環等待,再加以分析,就能夠逆向推斷出循環等待的成因,也就是死鎖造成的緣由。

死鎖造成流程圖

爲了讓你們更好地理解死鎖造成的緣由,咱們再經過表格的形式闡述死鎖造成的流程:

拓展

在排查死鎖的過程當中,有個同事還發現了上述場景會產生另外一種死鎖,該場景沒法經過手工復現,只有高併發場景下才有可能復現。

該死鎖對應的日誌這裏就不貼出了,與上一個死鎖的核心差異是事務2等待的鎖從S鎖換成了X鎖,也就是lock_mode X locks gap before rec insert intention waiting。咱們仍是經過表格來詳細說明該死鎖產生的流程:

總結

排查死鎖時,首先須要根據死鎖日誌分析循環等待的場景,而後根據當前各個事務執行的SQL分析出加鎖類型以及順序,逆向推斷出如何造成循環等待,這樣就能找到死鎖產生的緣由了。

PS:上述分析都是基於經驗的推斷,但願其餘小夥伴們可以指出當中的錯誤以及不足指出,謝謝!

讀者福利:

針對於Java程序員,我這邊準備了免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)有須要的程序員朋友能夠加Java高級架構攻城獅:519752913爲何某些人會一直比你優秀,是由於他自己就很優秀還一直在持續努力變得更優秀,而你是否是還在知足於現狀心裏在竊喜!但願讀到這的您能點個小贊和關注下我,之後還會更新技術乾貨,謝謝您的支持!

相關文章
相關標籤/搜索