之前接觸到的數據庫死鎖,都是批量更新時加鎖順序不一致而致使的死鎖,可是上週卻遇到了一個很難理解的死鎖。藉着這個機會又從新學習了一下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爲何某些人會一直比你優秀,是由於他自己就很優秀還一直在持續努力變得更優秀,而你是否是還在知足於現狀心裏在竊喜!但願讀到這的您能點個小贊和關注下我,之後還會更新技術乾貨,謝謝您的支持!