InnoDB的鎖機制淺析(五)—死鎖場景(Insert死鎖)

可能的死鎖場景

文章總共分爲五個部分:html

大而全版(五合一):InnoDB的鎖機制淺析(All in One)mysql

前言

這一章節只列舉兩種死鎖場景,其餘的死鎖問題大多也萬變不離其宗。sql

示例的基礎是一個只有兩列的數據庫表。數據庫

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引起。ui

1.1 rollback引起的Duplicate key死鎖

我命名爲insert-insert-insert-rollback死鎖rest

事務一 事務二 事務三
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的死鎖檢測一旦檢測到死鎖發生,會自動失敗其中一個事務,所以看到的結果是一個失敗另外一個成功。code

爲何會死鎖?htm

死鎖產生的緣由是事務一插入記錄時,對(2,2)記錄加X鎖,此時事務二和事務三插入數據時檢測到了重複鍵錯誤,此時事務二和事務三要在這條索引記錄上設置S鎖,因爲X鎖的存在,S鎖的獲取被阻塞。
事務一回滾,因爲S鎖和S鎖是能夠兼容的,所以事務二和事務三都得到了這條記錄的S鎖,此時其中一個事務但願插入,則該事務指望在這條記錄上加上X鎖,然而另外一個事務持有S鎖,S鎖和X鎖互相是不兼容的,兩個事務就開始互相等待對方的鎖釋放,形成了死鎖。blog

事務二和事務三爲何會加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 waitinglock_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隔離級別下出現死鎖以外還會致使其餘兩個問題:

  • 第一個是可重複讀可能會由於此次的當前讀操做而中斷,(一樣,幻讀可能也會所以產生);
  • 第二個是其餘事務的更新可能會丟失(解決方式:悲觀鎖、樂觀鎖)。
相關文章
相關標籤/搜索