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 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隔離級別下出現死鎖之外還會導致其他兩個問題:

  • 第一個是可重復讀可能會因為這次的當前讀操作而中斷,(同樣,幻讀可能也會因此產生);
  • 第二個是其他事務的更新可能會丟失(解決方式:悲觀鎖、樂觀鎖)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM