可能的死鎖場景
文章總共分為五個部分:
- InnoDB的鎖機制淺析(一)—基本概念/兼容矩陣
- InnoDB的鎖機制淺析(二)—探索InnoDB中的鎖(Record鎖/Gap鎖/Next-key鎖/插入意向鎖)
- InnoDB的鎖機制淺析(三)—幻讀
- InnoDB的鎖機制淺析(四)—不同SQL的加鎖狀況
- InnoDB的鎖機制淺析(五)—死鎖場景(Insert死鎖)
大而全版(五合一):InnoDB的鎖機制淺析(All in One)
前言
這一章節只列舉兩種死鎖場景,其他的死鎖問題大多也萬變不離其宗。
示例的基礎是一個只有兩列的數據庫表。
mysql> CREATE TABLE test (
id int(11) NOT NULL,
code int(11) NOT NULL,
PRIMARY KEY(id),
KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
mysql> INSERT INTO test(id,code) values(1,1),(10,10);
鎖的兼容矩陣如下:
| --- | 排它鎖(X) | 意向排它鎖(IX) | 共享鎖(S) | 意向共享鎖(IS) |
|---|---|---|---|---|
| 排它鎖(X) | N | N | N | N |
| 意向排它鎖(IX) | N | OK | N | OK |
| 共享鎖(S) | N | N | OK | OK |
| 意向共享鎖(IS) | N | OK | OK | OK |
1 Duplicate key error引發的死鎖
並發條件下,唯一鍵索引沖突可能會導致死鎖,這種死鎖一般分為兩種,一種是rollback引發,另一種是commit引發。
1.1 rollback引發的Duplicate key死鎖
我命名為insert-insert-insert-rollback死鎖
| 事務一 | 事務二 | 事務三 |
|---|---|---|
| mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); Query OK, 1 row affected (0.01 sec) |
||
| mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執行之后被阻塞,等待事務一 |
||
| mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執行之后被阻塞,等待事務一 |
||
| mysql>rollback; Query OK, 0 rows affected (0.00 sec) |
||
| ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | ||
| Query OK, 1 row affected (16.13 sec) |
當事務一執行回滾時,事務二和事務三發生了死鎖。InnoDB的死鎖檢測一旦檢測到死鎖發生,會自動失敗其中一個事務,因此看到的結果是一個失敗另一個成功。
為什么會死鎖?
死鎖產生的原因是事務一插入記錄時,對(2,2)記錄加X鎖,此時事務二和事務三插入數據時檢測到了重復鍵錯誤,此時事務二和事務三要在這條索引記錄上設置S鎖,由於X鎖的存在,S鎖的獲取被阻塞。
事務一回滾,由於S鎖和S鎖是可以兼容的,因此事務二和事務三都獲得了這條記錄的S鎖,此時其中一個事務希望插入,則該事務期望在這條記錄上加上X鎖,然而另一個事務持有S鎖,S鎖和X鎖互相是不兼容的,兩個事務就開始互相等待對方的鎖釋放,造成了死鎖。
事務二和事務三為什么會加S鎖,而不是直接等待X鎖
事務一的insert語句加的是隱式鎖(隱式的Record鎖、X鎖),但是其他事務插入同一行記錄時,出現了唯一鍵沖突,事務一的隱式鎖升級為顯示鎖。
事務二和事務三在插入之前判斷到了唯一鍵沖突,是因為插入前的重復索引檢查,這次檢查必須進行一次當前讀,於是非唯一索引就會被加上S模式的next-key鎖,唯一索引就被加上了S模式的Record鎖。
因為插入和更新之前都要進行重復索引檢查而執行當前讀操作,所以RR隔離級別下,同一個事務內不連續的查詢,可能也會出現幻讀的效果(但個人並不認為RR級別下也會出現幻讀,幻讀的定義應該是連續的讀取)。而連續的查詢由於都是讀取快照,中間沒有當前讀的操作,所以不會出現幻讀。
1.2 commit引發的Duplicate key死鎖
delete-insert-insert-commit死鎖
| 事務一 | 事務二 | 事務三 |
|---|---|---|
| mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from test where id=2; Query OK, 1 row affected (0.01 sec) |
||
| mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執行之后被阻塞,等待事務一 |
||
| mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執行之后被阻塞,等待事務一 |
||
| mysql>commit; Query OK, 0 rows affected (0.00 sec) |
||
| ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | ||
| Query OK, 1 row affected (2.37 sec) |
這種情況下產生的死鎖和insert-insert-insert-rollback死鎖產生的原理一致。
6.2 數據插入的過程
經過以上分析,一條數據在插入時經過以下幾個過程:
假設數據表test.test中存在(1,1)、(5,5)和(10,10)三條記錄。
- 事務開啟,嘗試獲取插入意向鎖。例如,事務一執行了
select * from test where id>8 for update,事務二要插入(9,9),此時先要獲取插入意向鎖,由於事務一已經在對應的記錄和間隙上加了X鎖,因此事務二被阻塞,並且阻塞的原因是獲取插入意向鎖時被事務一的X鎖阻塞。 - 獲取意向鎖之后,插入之前進行重復索引檢查。重復索引檢查為當前讀,需要添加S鎖。
- 如果是已經存在唯一索引,且索引未加鎖。直接拋出
Duplicate key的錯誤。如果存在唯一索引,且索引加鎖,等待鎖釋放。 - 重復檢查通過之后,加入X鎖,插入記錄
3 GAP與Insert Intention沖突引發死鎖
update-insert死鎖
仍然是表test,當前表中的記錄如下:
mysql> select * from test;
+----+------+
| id | code |
+----+------+
| 1 | 1 |
| 5 | 5 |
| 10 | 10 |
+----+------+
3 rows in set (0.01 sec)
| 事務一 | 事務二 |
|---|---|
| begin; | begin; |
| select * from test where id=5 for update; | select * from test where id=10 for update; |
| insert into test values(7,7); | |
| insert into test values(7,7); | |
| Query OK, 1 row affected (5.03 sec) | |
| ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
使用show engine innodb status查看死鎖狀態。先后出現lock_mode X locks gap before rec insert intention waiting和lock_mode X locks gap before rec字眼,是gap鎖和插入意向鎖的沖突導致的死鎖。
回顧select...for update的加鎖范圍
首先回顧一下兩個事務中的select ... for update做了哪些加鎖操作。
code=5時,首先會獲取code=5的索引記錄鎖(Record鎖),根據之前gap鎖的介紹,會在前一個索引和當前索引之間的間隙加鎖,於是區間(1,5)之間被加上了X模式的gap鎖。除此之外RR模式下,還會加next-key鎖,於是區間(5,10]被加了next-key鎖;
- 因此,
code=5的加鎖范圍是,區間(1,5)的gap鎖,{5}索引Record鎖,(5,10]的next-key鎖。即區間(1,10)上都被加上了X模式的鎖。 - 同理,
code=10的加鎖范圍是,區間(5,10)的gap鎖,{10}索引Record鎖,(10,+∞)的next-key鎖。
由gap鎖的特性,兼容矩陣中沖突的鎖也可以被不同的事務同時加在一個間隙上。上述兩個select ... for update語句出現了間隙鎖的交集,code=5的next-key鎖和code=10的gap鎖有重疊的區域——(5,10)。
死鎖的成因
當事務一執行插入語句時,會先加X模式的插入意向鎖,即兼容矩陣中的IX鎖。
但是由於插入意向鎖要鎖定的位置存在X模式的gap鎖。兼容矩陣中IX和X鎖是不兼容的,因此事務一的IX鎖會等待事務二的gap鎖釋放。
事務二也執行插入語句,與事務一同樣,事務二的插入意向鎖IX鎖會等待事務一的gap鎖釋放。
兩個事務互相等待對方先釋放鎖,因此出現死鎖。
2 總結
除了以上給出的幾種死鎖模式,還有很多其他死鎖的場景。
無論是哪種場景,萬變不離其宗,都是由於某個區間上或者某一個記錄上可以同時持有鎖,例如不同事務在同一個間隙gap上的鎖不沖突;不同事務中,S鎖可以阻塞X鎖的獲取,但是不會阻塞另一個事務獲取該S鎖。這樣才會出現兩個事務同時持有鎖,並互相等待,最終導致死鎖。
其中需要注意的點是,增、刪、改的操作都會進行一次當前讀操作,以此獲取最新版本的數據,並檢測是否有重復的索引。
這個過程除了會導致RR隔離級別下出現死鎖之外還會導致其他兩個問題:
- 第一個是可重復讀可能會因為這次的當前讀操作而中斷,(同樣,幻讀可能也會因此產生);
- 第二個是其他事務的更新可能會丟失(解決方式:悲觀鎖、樂觀鎖)。
