01 前言
遇到MySQL死鎖問題,我們應該怎么排查分析呢?之前線上出現一個insert on duplicate死鎖問題,本文將基于這個死鎖問題,分享排查分析過程,希望對大家有幫助。
02 死鎖案發還原
2.1 表結構
CREATE TABLE `song_rank` ( `id` int(11) NOT NULL AUTO_INCREMENT, `songId` int(11) NOT NULL, `weight` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `songId_idx` (`songId`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2 隔離級別
mysql> select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec)
2.3 數據庫版本
+------------+ | @@version | +------------+ | 5.7.21-log | +------------+ 1 row in set (0.00 sec)
2.4 關閉自動提交
mysql> select @@autocommit; +--------------+ | @@autocommit | +--------------+ | 1 | +--------------+ 1 row in set (0.00 sec) mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select @@autocommit; +--------------+ | @@autocommit | +--------------+ | 0 | +--------------+ 1 row in set (0.00 sec)
2.5 表中的數據
mysql> select * from song_rank; +----+--------+--------+ | id | songId | weight | +----+--------+--------+ | 1 | 10 | 30 | | 2 | 20 | 30 | +----+--------+--------+ 2 rows in set (0.01 sec)
2.6 死鎖案發原因
并發環境下,執行insert into … on duplicate key update…導致死鎖
2.7 死鎖模擬復現
(1)事務一執行
mysql> begin; //第一步 Query OK, 0 rows affected (0.00 sec) mysql> insert into song_rank(songId,weight) values(15,100) on duplicate key update weight=weight+1; //第二步 Query OK, 1 row affected (0.00 sec) mysql> rollback; //第七步 Query OK, 0 rows affected (0.00 sec)
(2)事務二執行
mysql> begin; //第三步 Query OK, 0 rows affected (0.00 sec) mysql> insert into song_rank(songId,weight) values(16,100) on duplicate key update weight=weight+1; // 第四步 Query OK, 1 row affected (40.83 sec)
(3)事務三執行
mysql> begin; //第五步 Query OK, 0 rows affected (0.00 sec) mysql> insert into song_rank(songId,weight) values(18,100) on duplicate key update weight=weight+1; //第六步
(4)事務一,事務二,事務三執行
死鎖浮出水面:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
03 死鎖破案排查分析
遇到死鎖問題時,我們應該怎么處理呢?有以下幾個步驟
3.1 查看死鎖日志
當數據庫發生死鎖時,可以通過以下命令獲取死鎖日志:
show engine innodb status;
上面例子insert on duplicate死鎖問題的日志如下:
*** (1) TRANSACTION: TRANSACTION 27540, ACTIVE 19 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1 MySQL thread id 23, OS thread handle 14896, query id 582 localhost ::1 root update insert into song_rank(songId,weight) values(18,100) on duplicate key update weight=weight+1 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 116 page no 4 n bits 72 index songId_idx of table `test2`.`song_rank` trx id 27540 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 80000014; asc ;; 1: len 4; hex 80000002; asc ;; *** (2) TRANSACTION: TRANSACTION 27539, ACTIVE 41 sec inserting, thread declared inside InnoDB 1 mysql tables in use 1, locked 1 4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1 MySQL thread id 22, OS thread handle 6976, query id 580 localhost ::1 root update insert into song_rank(songId,weight) values(16,100) on duplicate key update weight=weight+1 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 116 page no 4 n bits 72 index songId_idx of table `test2`.`song_rank` trx id 27539 lock_mode X locks gap before rec Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 80000014; asc ;; 1: len 4; hex 80000002; asc ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 116 page no 4 n bits 72 index songId_idx of table `test2`.`song_rank` trx id 27539 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 80000014; asc ;; 1: len 4; hex 80000002; asc ;;
3.2 分析死鎖日志
如何分析死鎖日志呢? 分享一下我的思路
- 死鎖日志分事務1,事務2拆分
- 找出發生死鎖的SQL
- 找出事務持有什么鎖,都在等待什么鎖
- SQL加鎖分析
(1)事務1日志分析
從日志我們可以看到事務1正在執行的SQL為:
insert into song_rank(songId,weight) values(18,100) on duplicate key update weight=weight+1
該條語句正在等待索引songId_idx的插入意向排他鎖:
lock_mode X locks gap before rec insert intention waiting
(2)事務2日志分析
從日志我們可以看到事務2正在執行的SQL為:
insert into song_rank(songId,weight) values(16,100) on duplicate key update weight=weight+1
該語句持有一個索引songId_idx的間隙鎖:
lock_mode X locks gap before rec
該條語句正在等待索引songId_idx的插入意向排他鎖:
lock_mode X locks gap before rec insert intention waiting
(3)鎖相關概念補充(附)
考慮到有些讀者可能對上面insert intention鎖等不太熟悉,所以這里這里補一小節鎖相關概念。 官方文檔
InnoDB 鎖類型思維導圖:
我們主要介紹一下兼容性以及鎖模式類型的鎖
① 共享鎖與排他鎖:
InnoDB 實現了標準的行級鎖,包括兩種:共享鎖(簡稱 s 鎖)、排它鎖(簡稱 x 鎖)。
- 共享鎖(S鎖):允許持鎖事務讀取一行。
- 排他鎖(X鎖):允許持鎖事務更新或者刪除一行。
如果事務 T1 持有行 r 的 s 鎖,那么另一個事務 T2 請求 r 的鎖時,會做如下處理:
- T2 請求 s 鎖立即被允許,結果 T1 T2 都持有 r 行的 s 鎖
- T2 請求 x 鎖不能被立即允許
如果 T1 持有 r 的 x 鎖,那么 T2 請求 r 的 x、s 鎖都不能被立即允許,T2 必須等待T1釋放 x 鎖才可以,因為X鎖與任何的鎖都不兼容。
② 意向鎖
- 意向共享鎖( IS 鎖):事務想要獲得一張表中某幾行的共享鎖
- 意向排他鎖( IX 鎖): 事務想要獲得一張表中某幾行的排他鎖
比如:事務1在表1上加了S鎖后,事務2想要更改某行記錄,需要添加IX鎖,由于不兼容,所以需要等待S鎖釋放;如果事務1在表1上加了IS鎖,事務2添加的IX鎖與IS鎖兼容,就可以操作,這就實現了更細粒度的加鎖。
InnoDB存儲引擎中鎖的兼容性如下表:
③ 記錄鎖(Record Locks)
- 記錄鎖是最簡單的行鎖,僅僅鎖住一行。如:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
- 記錄鎖永遠都是加在索引上的,即使一個表沒有索引,InnoDB也會隱式的創建一個索引,并使用這個索引實施記錄鎖。
- 會阻塞其他事務對其插入、更新、刪除
記錄鎖的事務數據(關鍵詞:lock_mode X locks rec but not gap),記錄如下:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` trx id 10078 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000000274f; asc 'O;; 2: len 7; hex b60000019d0110; asc ;;
④ 間隙鎖(Gap Locks)
- 間隙鎖是一種加在兩個索引之間的鎖,或者加在第一個索引之前,或最后一個索引之后的間隙。
- 使用間隙鎖鎖住的是一個區間,而不僅僅是這個區間中的每一條數據。
- 間隙鎖只阻止其他事務插入到間隙中,他們不阻止其他事務在同一個間隙上獲得間隙鎖,所以 gap x lock 和 gap s lock 有相同的作用。
⑤ Next-Key Locks
- Next-key鎖是記錄鎖和間隙鎖的組合,它指的是加在某條記錄以及這條記錄前面間隙上的鎖。
⑥ 插入意向鎖(Insert Intention)
- 插入意向鎖是在插入一行記錄操作之前設置的一種間隙鎖,這個鎖釋放了一種插入方式的信號,亦即多個事務在相同的索引間隙插入時如果不是插入間隙中相同的位置就不需要互相等待。
- 假設有索引值4、7,幾個不同的事務準備插入5、6,每個鎖都在獲得插入行的獨占鎖之前用插入意向鎖各自鎖住了4、7之間的間隙,但是不阻塞對方因為插入行不沖突。
事務數據類似于下面:
RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child` trx id 8731 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 4; hex 80000066; asc f;; 1: len 6; hex 000000002215; asc " ;; 2: len 7; hex 9000000172011c; asc r ;;...
(4)SQL加鎖分析
通過分析死鎖日志,我們可以找到發生死鎖的SQL,以及相關等待的鎖,我們再對對應的SQL進行加鎖分析,其實問題就迎刃而解了。
OK,我們回到對應的SQL,insert into song_rank(songId,weight) values(16,100) on duplicate key update weight=weight+1 執行過程到底加了什么鎖呢?
① insert加鎖策略
insert語句會對插入的這條記錄加排他記錄鎖,在加記錄鎖之前還會加一種 GAP 鎖,叫做插入意向(insert intention)鎖,如果出現唯一鍵沖突,還會加一個共享記錄(S)鎖。
(SQL加鎖分析非常重要,在這里給大家推薦一篇文章,講的非常好,解決死鎖之路 - 常見 SQL 語句的加鎖分析)
② insert on duplicate key加鎖驗證
為了驗證一下insert on duplicate key加鎖情況,我們拿上面demo的事務1和2在走一下流程。
事務1:
mysql> begin; //第一步 Query OK, 0 rows affected (0.00 sec) mysql> insert into song_rank(songId,weight) values(15,100) on duplicate key update weight=weight+1; //第二步 Query OK, 1 row affected (0.00 sec)
事務2(另開窗口):
mysql> begin; //第三步 Query OK, 0 rows affected (0.00 sec) mysql> insert into song_rank(songId,weight) values(16,100) on duplicate key update weight=weight+1; // 第四步
使用show engine innodb status查看當前鎖請求信息,如圖:
事務2持有:IX鎖(表鎖),gap x鎖,insert intention lock(在等待事務1的gap鎖)
所以,insert on duplicate 執行過程會上這三把鎖。
(5)死鎖原因分析
回歸到本文開頭介紹的死鎖案發模擬現場(事務1,2,3)以及死鎖日志現場,
案發后事務1的鎖:
案發后事務2的鎖:
案發復原路線:
首先,執行事務1執行: begin; insert into song_rank(songId,weight) values(15,100) on duplicate key update weight=weight+1; 會獲得 gap鎖(10,20),insert intention lock(插入意向鎖)
接著,事務2執行: begin; insert into song_rank(songId,weight) values(16,100) on duplicate key update weight=weight+1; 會獲得 gap鎖(10,20),同時等待事務1的insert intention lock(插入意向鎖)。
再然后,事務3執行: begin; insert into song_rank(songId,weight) values(18,100) on duplicate key update weight=weight+1; 會獲得 gap鎖(10,20),同時等待事務1的insert intention lock(插入意向鎖)。
最后,事務1回滾(rollback),釋放插入意向鎖,導致事務2,3同時持有gap鎖,等待insert intention鎖,死鎖形成!
鎖模式兼容矩陣(橫向是已持有鎖,縱向是正在請求的鎖):
兼容性 Gap Insert Intention Record Next-Key Gap 兼容 兼容 兼容 兼容 Insert Intention 沖突 兼容 兼容 沖突 Record 兼容 兼容 沖突 沖突 Next-Key 兼容 兼容 沖突 沖突 這是MySql5.7的一個bug
04 如何避免該insert on duplicate死鎖問題
4.1 把insert on duplicate改為insert
try{ insert(); }catch(DuplicateKeyException e){ update(); }
因為insert不會加gap鎖,所以可以避免該問題。
4.2 更改MySql版本
既然這是MySql5.7的一個bug,那么可以考慮更改Mysql版本。
4.3 盡量減少使用unique index
gap鎖跟索引有關,并且unique key 和foreign key會引起額外的index檢查,需要更大的開銷,所以我們盡量減少使用不必要的索引。
05 本文總結(重要)
本文介紹了MySql5.7死鎖的一個bug。我們應該怎樣去排查死鎖問題呢?
- show engine innodb status;查看死鎖日志
- 找出死鎖SQL
- SQL加鎖分析
- 分析死鎖日志(持有什么鎖,等待什么鎖)
- 熟悉鎖模式兼容矩陣,InnoDB存儲引擎中鎖的兼容性矩陣