共計 5371 個字符,預計需要花費 14 分鐘才能閱讀完成。
這篇文章主要介紹“INSERT 語句引發的死鎖實例分析”,在日常操作中,相信很多人在 INSERT 語句引發的死鎖實例分析問題上存在疑惑,丸趣 TV 小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”INSERT 語句引發的死鎖實例分析”的疑惑有所幫助!接下來,請跟著丸趣 TV 小編一起來學習吧!
兩條一樣的 INSERT 語句竟然引發了死鎖,這究竟是人性的扭曲,還是道德的淪喪,讓我們不禁感嘆一句:臥槽!這也能死鎖,然后眼中含著悲催的淚水無奈的改起了業務代碼。
好的,在深入分析為啥兩條一樣的 INSERT 語句也會產生死鎖之前,我們先介紹一些基礎知識。
準備一下環境
為了故事的順利發展,我們新建一個用了無數次的 hero 表:
CREATE TABLE hero (
number INT AUTO_INCREMENT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number),
UNIQUE KEY uk_name (name)
) Engine=InnoDB CHARSET=utf8;
然后向這個表里插入幾條記錄:
INSERT INTO hero VALUES
(1, l 劉備 , 蜀),
(3, z 諸葛亮 , 蜀),
(8, c 曹操 , 魏),
(15, x 荀彧 , 魏),
(20, s 孫權 , 吳
現在 hero 表就有了兩個索引(一個唯一二級索引,一個聚簇索引),示意圖如下:
INSERT 語句如何加鎖
讀過《MySQL 是怎樣運行的:從根兒上理解 MySQL》的小伙伴肯定知道,INSERT 語句在正常執行時是不會生成鎖結構的,它是靠聚簇索引記錄自帶的 trx_id 隱藏列來作為隱式鎖來保護記錄的。
但是在一些特殊場景下,INSERT 語句還是會生成鎖結構的,我們列舉一下:
1. 待插入記錄的下一條記錄上已經被其他事務加了 gap 鎖時
每插入一條新記錄,都需要看一下待插入記錄的下一條記錄上是否已經被加了 gap 鎖,如果已加 gap 鎖,那 INSERT 語句應該被阻塞,并生成一個插入意向鎖。
比方說對于 hero 表來說,事務 T1 運行在 REPEATABLE READ(后續簡稱為 RR,后續也會把 READ COMMITTED 簡稱為 RC)隔離級別中,執行了下邊的語句:
# 事務 T1
mysql BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql SELECT * FROM hero WHERE number 8 FOR UPDATE;
+--------+------------+---------+
| number | name | country |
+--------+------------+---------+
| 1 | l 劉備 | 蜀 |
| 3 | z 諸葛亮 | 蜀 |
+--------+------------+---------+
2 rows in set (0.02 sec)
這條語句會對主鍵值為 1、3、8 的這 3 條記錄都添加 X 型 next-key 鎖,不信的話我們使用 SHOW ENGINE INNODB STATUS 語句看一下加鎖情況,圖中箭頭指向的記錄就是 number 值為 8 的記錄:
小貼士:
至于 SELECT、DELETE、UPDATE 語句如何加鎖,我們已經在之前的文章中分析過了,這里就不再贅述了。
此時事務 T2 想插入一條主鍵值為 4 的聚簇索引記錄,那么 T2 在插入記錄前,首先要定位一下主鍵值為 4 的聚簇索引記錄在頁面中的位置,發現主鍵值為 4 的下一條記錄的主鍵值是 8,而主鍵值是 8 的聚簇索引記錄已經被添加了 gap 鎖(next-key 鎖包含了正經記錄鎖和 gap 鎖),那么事務 1 就需要進入阻塞狀態,并生成一個類型為插入意向鎖的鎖結構。
我們在事務 T2 中執行一下 INSERT 語句驗證一下:
mysql BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql INSERT INTO hero VALUES(4, g 關羽 , 蜀
此時 T2 進入阻塞狀態,我們再使用 SHOW ENGINE INNODB STATUS 看一下加鎖情況:
可見 T2 對主鍵值為 8 的聚簇索引記錄加了一個插入意向鎖(就是箭頭處指向的 lock_mode X locks gap before rec insert intention),并且處在 waiting 狀態。
好了,驗證過之后,我們再來看看代碼里是如何實現的:
lock_rec_insert_check_and_lock 函數用于看一下別的事務是否阻止本次 INSERT 插入,如果是,那么本事務就給被別的事務添加了 gap 鎖的記錄生成一個插入意向鎖,具體過程如下:
小貼士:
lock_rec_other_has_conflicting 函數用于檢測本次要獲取的鎖和記錄上已有的鎖是否有沖突,有興趣的同學可以看一下。
2. 遇到重復鍵時
如果在插入新記錄時,發現頁面中已有的記錄的主鍵或者唯一二級索引列與待插入記錄的主鍵或者唯一二級索引列值相同(不過可以有多條記錄的唯一二級索引列的值同時為 NULL,這里不考慮這種情況了),此時插入新記錄的事務會獲取頁面中已存在的鍵值相同的記錄的鎖。
如果是主鍵值重復,那么:
當隔離級別不大于 RC 時,插入新記錄的事務會給已存在的主鍵值重復的聚簇索引記錄添加 S 型正經記錄鎖。
當隔離級別不小于 RR 時,插入新記錄的事務會給已存在的主鍵值重復的聚簇索引記錄添加 S 型 next-key 鎖。
如果是唯一二級索引列重復,那不論是哪個隔離級別,插入新記錄的事務都會給已存在的二級索引列值重復的二級索引記錄添加 S 型 next-key 鎖,再強調一遍,加的是 next-key 鎖!加的是 next-key 鎖!加的是 next-key 鎖!這是 rc 隔離級別中為數不多的給記錄添加 gap 鎖的場景。
小貼士:
本來設計 InnoDB 的大叔并不想在 RC 隔離級別引入 gap 鎖,但是由于某些原因,如果不添加 gap 鎖的話,會讓唯一二級索引中出現多條唯一二級索引列值相同的記錄,這就違背了 UNIQUE 約束。所以后來設計 InnoDB 的大叔就很不情愿的在 RC 隔離級別也引入了 gap 鎖。
我們也來做一個實驗,現在假設上邊的 T1 和 T2 都回滾了,現在將隔離級別調至 RC,重新開啟事務進行測試。
mysql SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.01 sec)
# 事務 T1
mysql BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql INSERT INTO hero VALUES(30, x 荀彧 , 魏
ERROR 1062 (23000): Duplicate entry x 荀彧 for key uk_name
然后執行 SHOW ENGINE INNODB STATUS 語句看一下 T1 加了什么鎖:
可以看到即使現在 T1 的隔離級別為 RC,T1 仍然給 name 列值為 x 荀彧 的二級索引記錄添加了 S 型 next-key 鎖(圖中紅框中的 lock mode S)。
如果我們的 INSERT 語句還帶有 ON DUPLICATE KEY… 這樣的子句,如果遇到主鍵值或者唯一二級索引列值重復的情況,會對 B + 樹中已存在的相同鍵值的記錄加 X 型鎖,而不是 S 型鎖(不過具體鎖的具體類型是和前面描述一樣的)。
好了,又到了看代碼求證時間了,我們看一下吧:
row_ins_scan_sec_index_for_duplicate 是檢測唯一二級索引列值是否重復的函數,具體加鎖的代碼如下所示:
如上圖所示,在遇到唯一二級索引列重復的情況時:
1 號紅框表示對帶有 ON DUPLICATE … 子句時的處理方案,具體就是添加 X 型鎖。
2 號紅框表示對正常 INSERT 語句的處理方案,具體就是添加 S 型鎖。
不過不論是那種情況,添加的 lock_typed 的值都是 LOCK_ORDINARY,表示 next-key 鎖。
在主鍵重復時 INSERT 語句的加鎖代碼我們就不列舉了。
3. 外鍵檢查時
當我們向子表中插入記錄時,我們分兩種情況討論:
當子表中的外鍵值可以在父表中找到時,那么無論當前事務是什么隔離級別,只需要給父表中對應的記錄添加一個 S 型正經記錄鎖就好了。
當子表中的外鍵值在父表中找不到時:那么如果當前隔離級別不大于 RC 時,不對父表記錄加鎖;當隔離級別不小于 RR 時,對父表中該外鍵值所在位置的下一條記錄添加 gap 鎖。
死鎖要出場了
好了,基礎知識預習完了,該死鎖出場了。
看下邊這個平平無奇的 INSERT 語句:
INSERT INTO hero(name, country) VALUES(g 關羽 , 蜀), (d 鄧艾 , 魏
這個語句用來插入兩條記錄,不論是在 RC,還是 RR 隔離級別,如果兩個事務并發執行它們是有一定幾率觸發死鎖的。為了穩定復現這個死鎖,我們把上邊一條語句拆分成兩條語句:
INSERT INTO hero(name, country) VALUES( g 關羽 , 蜀
INSERT INTO hero(name, country) VALUES(d 鄧艾 , 魏
拆分前和拆分后起到的作用是相同的,只不過拆分后我們可以人為的控制插入記錄的時機。如果 T1 和 T2 的執行順序是這樣的:
也就是:
T1 先插入 name 值為 g 關羽的記錄,可以插入成功,此時對應的唯一二級索引記錄被隱式鎖保護,我們執行 SHOW ENGINE INNODB STATUS 語句,發現啥一個行鎖(row lock)都沒有(因為 SHOW ENGINE INNODB STATUS 不顯示隱式鎖):
接著 T2 也插入 name 值為 g 關羽的記錄。由于 T1 已經插入 name 值為 g 關羽的記錄,所以 T2 在插入二級索引記錄時會遇到重復的唯一二級索引列值,此時 T2 想獲取一個 S 型 next-key 鎖,但是 T1 并未提交,T1 插入的 name 值為 g 關羽的記錄上的隱式鎖相當于一個 X 型正經記錄鎖(RC 隔離級別),所以 T2 向獲取 S 型 next-key 鎖時會遇到鎖沖突,T2 進入阻塞狀態,并且將 T1 的隱式鎖轉換為顯式鎖(就是幫助 T1 生成一個正經記錄鎖的鎖結構)。這時我們再執行 SHOW ENGINE INNODB STATUS 語句:
可見,T1 持有的 name 值為 g 關羽的隱式鎖已經被轉換為顯式鎖(X 型正經記錄鎖,lock_mode X locks rec but not gap);T2 正在等待獲取一個 S 型 next-key 鎖(lock mode S waiting)。
接著 T1 再插入一條 name 值為 d 鄧艾的記錄。在插入一條記錄時,會在頁面中先定位到這條記錄的位置。在插入 name 值為 d 鄧艾的二級索引記錄時,發現現在頁面中的記錄分布情況如下所示:
很顯然,name 值為 d 鄧艾 的二級索引記錄所在位置的下一條二級索引記錄的 name 值應該是 g 關羽(按照漢語拼音排序)。那么在 T1 插入 name 值為 d 鄧艾的二級索引記錄時,就需要看一下 name 值為 g 關羽 的二級索引記錄上有沒有被別的事務加 gap 鎖。
有同學想說:目前只有 T2 想在 name 值為 g 關羽 的二級索引記錄上添加 S 型 next-key 鎖(next-key 鎖包含 gap 鎖),但是 T2 并沒有獲取到鎖呀,目前正在等待狀態。那么 T1 不是能順利插入 name 值為 g 關羽 的二級索引記錄么?
我們看一下執行結果:
# 事務 T2
mysql INSERT INTO hero(name, country) VALUES( g 關羽 , 蜀
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
很顯然,觸發了一個死鎖,T2 被 InnoDB 回滾了。
這是為啥呢?T2 明明沒有獲取到 name 值為 g 關羽 的二級索引記錄上的 S 型 next-key 鎖,為啥 T1 還不能插入入 name 值為 d 鄧艾的二級索引記錄呢?
這我們還得回到代碼上來,看一下插入新記錄時是如何判斷鎖是否沖突的:
看一下畫紅框的注釋,意思是:只要別的事務生成了一個顯式的 gap 鎖的鎖結構,不論那個事務已經獲取到了該鎖 (granted),還是正在等待獲取(waiting),當前事務的 INSERT 操作都應該被阻塞。
回到我們的例子中來,就是 T2 已經在 name 值為 g 關羽 的二級索引記錄上生成了一個 S 型 next-key 鎖的鎖結構,雖然 T2 正在阻塞(尚未獲取鎖),但是 T1 仍然不能插入 name 值為 d 鄧艾的二級索引記錄。
這樣也就解釋了死鎖產生的原因:
T1 在等待 T2 釋放 name 值為 g 關羽 的二級索引記錄上的 gap 鎖。
T2 在等待 T1 釋放 name 值為 g 關羽 的二級索引記錄上的 X 型正經記錄鎖。
兩個事務相互等待對方釋放鎖,這樣死鎖也就產生了。
怎么解決這個死鎖問題?
兩個方案:
方案一:一個事務中只插入一條記錄。
方案二:先插入 name 值為 d 鄧艾 的記錄,再插入 name 值為 g 關羽 的記錄
到此,關于“INSERT 語句引發的死鎖實例分析”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注丸趣 TV 網站,丸趣 TV 小編會繼續努力為大家帶來更多實用的文章!