遇到Mysql死鎖問題,咱們應該怎麼排查分析呢?以前線上出現一個insert on duplicate死鎖問題,本文將基於這個死鎖問題,分享排查分析過程,但願對你們有幫助。php
表結構:html
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;
複製代碼
隔離級別:mysql
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
複製代碼
數據庫版本:sql
+------------+
| @@version |
+------------+
| 5.7.21-log |
+------------+
1 row in set (0.00 sec)
複製代碼
關閉自動提交:數據庫
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)
複製代碼
表中的數據:segmentfault
mysql> select * from song_rank;
+----+--------+--------+
| id | songId | weight |
+----+--------+--------+
| 1 | 10 | 30 |
| 2 | 20 | 30 |
+----+--------+--------+
2 rows in set (0.01 sec)
複製代碼
死鎖案發緣由:bash
併發環境下,執行insert into … on duplicate key update…致使死鎖併發
死鎖模擬復現:ide
事務一執行:學習
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)
複製代碼
事務二執行:
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)
複製代碼
事務三執行:
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; //第六步
複製代碼
事務一,事務二,事務三執行:
步驟 | 事務一 | 事務二 | 事務三 |
---|---|---|---|
第一步 | begin; | ||
第二步 | insert into song_rank(songId,weight) values(15,100) on duplicate key update weight=weight+1; (Query OK, 1 row affected (0.00 sec) ) | ||
第三步 | begin; | ||
第四步 | insert into song_rank(songId,weight) values(16,100) on duplicate key update weight=weight+1; //被阻塞 | ||
第五步 | begin; | ||
第六步 | insert into song_rank(songId,weight) values(18,100) on duplicate key update weight=weight+1; //被阻塞 | ||
第七步 | rollback; | ||
結果 | Query OK, 1 row affected (40.83 sec) | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
死鎖浮出水面:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
複製代碼
遇到死鎖問題時,咱們應該怎麼處理呢?分一下幾個步驟
當數據庫發生死鎖時,能夠經過如下命令獲取死鎖日誌:
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 ;;
複製代碼
如何分析死鎖日誌呢? 分享一下個人思路
從日誌咱們能夠看到事務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正在執行的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
複製代碼
考慮到有些讀者可能對上面insert intention鎖等不太熟悉,因此這裏這裏補一小節鎖相關概念。 官方文檔
InnoDB 鎖類型思惟導圖:
咱們主要介紹一下兼容性以及鎖模式類型的鎖
1.共享鎖與排他鎖:
InnoDB 實現了標準的行級鎖,包括兩種:共享鎖(簡稱 s 鎖)、排它鎖(簡稱 x 鎖)。
若是事務 T1 持有行 r 的 s 鎖,那麼另外一個事務 T2 請求 r 的鎖時,會作以下處理:
若是 T1 持有 r 的 x 鎖,那麼 T2 請求 r 的 x、s 鎖都不能被當即容許,T2 必須等待T1釋放 x 鎖才能夠,由於X鎖與任何的鎖都不兼容。
2.意向鎖
好比:事務1在表1上加了S鎖後,事務2想要更改某行記錄,須要添加IX鎖,因爲不兼容,因此須要等待S鎖釋放;若是事務1在表1上加了IS鎖,事務2添加的IX鎖與IS鎖兼容,就能夠操做,這就實現了更細粒度的加鎖。
InnoDB存儲引擎中鎖的兼容性以下表:
兼容性 | IS | IX | S | X |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
3.記錄鎖(Record Locks)
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
記錄鎖的事務數據(關鍵詞: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 ;; 複製代碼
4.間隙鎖(Gap Locks)
5.Next-Key Locks
6.插入意向鎖(Insert Intention)
事務數據相似於下面:
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 ;;... 複製代碼
經過分析死鎖日誌,咱們能夠找到發生死鎖的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 執行過程會上這三把鎖。
迴歸到本文開頭介紹的死鎖案發模擬現場(事務1,2,3)以及死鎖日誌現場,
案發後事務1的鎖:
案發復原路線:
1.首先,執行事務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.接着,事務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.再而後,事務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(插入意向鎖)。
4.最後,事務1回滾(rollback),釋放插入意向鎖,致使事務2,3同時持有gap鎖,等待insert intention鎖,死鎖造成!
鎖模式兼容矩陣(橫向是已持有鎖,縱向是正在請求的鎖):
兼容性 | Gap | Insert Intention | Record | Next-Key |
---|---|---|---|---|
Gap | 兼容 | 兼容 | 兼容 | 兼容 |
Insert Intention | 衝突 | 兼容 | 兼容 | 衝突 |
Record | 兼容 | 兼容 | 衝突 | 衝突 |
Next-Key | 兼容 | 兼容 | 衝突 | 衝突 |
try{
insert();
}catch(DuplicateKeyException e){
update();
}
複製代碼
由於insert不會加gap鎖,因此能夠避免該問題。
既然這是MySql5.7的一個bug,那麼能夠考慮更改Mysql版本。
gap鎖跟索引有關,而且unique key 和foreign key會引發額外的index檢查,須要更大的開銷,因此咱們儘可能減小使用沒必要要的索引。
本文介紹了MySql5.7死鎖的一個bug。咱們應該怎樣去排查死鎖問題呢?