1. 程式人生 > 其它 >(11)MySQL進階篇SQL優化(InnoDB鎖問題排查與解決)

(11)MySQL進階篇SQL優化(InnoDB鎖問題排查與解決)

1.概述

前面章節之所以介紹那麼多鎖的知識點和示例,其實最終目的就是為了排查與解決死鎖的問題,下面我們把之前學過鎖知識重溫與補充一遍,然後再通過例子演示下如果排查與解決死鎖。

2.前期準備

●資料庫事務隔離級別

SHOW VARIABLES LIKE 'transaction_isolation%';


MYSQL事務隔離級別預設可重複讀(如果還不瞭解事務隔離級別的鞋童們,可以移步到我寫這篇文章去了解下)。
●將事務自動提交關閉

SET AUTOCOMMIT=0;

事務自動提交配置:0.事務非自動提交,1.事務自動提交
●建立一個模擬演示用的會員表

CREATE TABLE goods.members (`ID` int
NOT NULL AUTO_INCREMENT COMMENT '會員自增ID',`MemberName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '會員名稱',`Tel` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '手機號碼',PRIMARY KEY (`ID`));

●在MemberName會員名稱欄位上建立一個非聚集索引

ALTER TABLE goods.members ADD INDEX IX_MemberName(MemberName);
SHOW INDEX FROM goods.members;


●往會員表插入四條資料,方便間隙鎖跟記錄鎖例子演示

INSERT INTO goods.members (MemberName,Tel) VALUES ('A','110'),('B','120'),('C','130'),('D','140');
SELECT * FROM goods.members;


好了,前期條件已經準備完畢,在演示之前,下面讓我們來重溫與補充下鎖知識。

3.鎖知識重溫與補充

3.1鎖的介紹

下面就根據上述圖再次重溫與補充下之前學習過鎖的知識點。

3.2樂觀鎖與悲觀鎖

悲觀鎖與樂觀鎖是兩種常見的資源併發鎖設計思路,也是併發程式設計中一個非常基礎的概念。
●悲觀鎖(Pessimistic Lock)


悲觀鎖的特點是先獲取鎖,再進行業務操作,即“悲觀”的認為獲取鎖是非常有可能失敗的,因此要先確保獲取鎖成功再進行業務操作。通常所說的“一鎖二查三更新”即指的是使用悲觀鎖。通常來講在資料庫上的悲觀鎖需要資料庫本身提供支援,即通過常用的select...for update操作來實現悲觀鎖。當資料庫執行select for update時會獲取被select中的資料行的行鎖,因此其他併發執行的select for update如果試圖選中同一行則會發生排斥(需要等待行鎖被釋放),因此達到鎖的效果。select for update獲取的行鎖會在當前事務結束時自動釋放,因此必須在事務中使用。
●樂觀鎖(Optimistic Lock)
樂觀鎖的特點先進行業務操作,不到萬不得已不去拿鎖。即“樂觀”的認為拿鎖多半是會成功的,因此在進行完業務操作需要實際更新資料的最後一步再去拿一下鎖就好。樂觀鎖在資料庫上的實現完全是邏輯的,不需要資料庫提供特殊的支援。一般的做法是在需要鎖的資料上增加一個版本號,或者時間戳。例如UPDATE SET data = new_data, version = new_version WHERE version = old_version;

3.3共享鎖與排他鎖

InnoDB儲存引擎有主要兩種型別的行鎖:
●共享鎖(S鎖):允許持鎖事務讀取資料行。
●排他鎖(X鎖):允許持鎖事務更新或者刪除資料行。
假設事務T1持有R記錄行S鎖,事務T2請求獲取R記錄行時,會做如下處理:
◎T2請求S鎖會被允許,結果T1,T2都會持有R記錄S鎖。
◎T2請求X鎖不會允許,需要等待T1釋放S鎖。
同理,假設事務T1持有R記錄行X鎖,事務T2請求持有R記錄行S、X鎖時,會做如下處理:
◎T2必須等待T1釋放X鎖才可以操作R記錄行,因為S鎖與X鎖不相容。

3.4意向鎖

●意向共享鎖(IS鎖):允許事務獲取表資料行的共享鎖。
●意向排他鎖(IX鎖):允許事務獲取表資料行的排他鎖。
假設事務T1在某表上加了S鎖,事務T2想要更改該表R記錄行時,要先新增IX鎖:
◎由於S鎖與IX鎖不相容,所以需要等待T1釋放S鎖才能更改該表R記錄行。
同理,假設事務T1在某表上加了IS鎖,事務T2想要更改該表R記錄行時,添加了IX鎖:
◎由於IS鎖與IX鎖相容,所以事務T2可以更改該表R記錄行,這樣也實現了鎖多粒度。
InnoDB儲存引擎鎖相容性如下:

3.5記錄鎖(Record Locks)

●它是建立在索引記錄上的行鎖,會鎖住一行記錄:SELECT * FROM goods.members WHERE ID=1 FOR UPDATE;
●當一條SQL沒有走任何索引時,那麼將會在每一條聚集索引後面加X鎖,這個類似於表鎖,但原理上和表鎖應該是完全不同的。
●即使查詢的表上沒有任何索引,InnoDB也會在後臺建立一個隱藏的聚集主鍵索引並實施記錄鎖。
●會阻塞其他事務的插入、更新和刪除。

RECORD LOCKS space id 51 page no 5 n bits 72 index IX_MemberName of table `goods`.`members` trx id 270900 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;

3.6間隙鎖(Gap Locks)

●僅僅鎖住一個索引區間(開區間)。其實就是索引項範圍內的間隙上鎖(在索引記錄之間的間隙中加鎖,或者是在某一條索引記錄之前或者之後加鎖,並不包括該索引記錄本身),避免幻讀。還有間隙鎖只會阻止其他事務插入到間隙當中,他們並不阻止其他事務在同一個間隙上獲得間隙鎖,所以gap x lock和gap s lock有相同的作用。如members表中ID主鍵間隙範圍:(-∞,1),(1,2),(2,3),(3,4), (4,+∞)。示例如下:
事務T1:

SELECT * FROM goods.members WHERE ID>1 AND ID<4 FOR UPDATE;


事務T2:

UPDATE goods.members SET Tel='110' WHERE ID IN (1,4);
UPDATE goods.members SET Tel='110' WHERE ID IN (2,3);


很明顯T1在主鍵ID (2,3)區間加了間隙鎖,當T1未釋放鎖情況下,T2想要更新ID>1 AND ID<4區間範圍值時,就會發生阻塞。

3.7臨鍵鎖(Next-Key Locks)

●臨鍵鎖(Next-Key Locks)其實也是一種特殊間隙鎖,是記錄鎖(Record Locks)和間隙鎖(Gap Locks)的組合。Next-Key鎖是在下一個索引記錄本身和索引之前的間隙加上S鎖或是X鎖(如果是讀就加上S鎖,如果是寫就加X鎖)。

3.8插入意向鎖(Insert Intention Locks)

Gap Lock中存在一種插入意向鎖(Insert Intention Lock),在insert操作時產生。在多事務同時寫入不同資料至同一索引間隙的時候,並不需要等待其他事務完成,不會發生鎖等待。
假設有一個記錄索引包含鍵值4和7,不同的事務分別插入5和6,每個事務都會產生一個加在4-7之間的插入意向鎖,獲取在插入行上的排它鎖,但是不會被互相鎖住,因為資料行並不衝突。

3.9行鎖的相容矩陣

4.死鎖

所謂死鎖,其實是指多個程序在執行過程中因爭奪資源而造成的一種僵持局面,當程序處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。如下圖所示:



因此我們舉個例子來描述,如果此時有一個事務A,先持有鎖A,再去獲得鎖B的情況下,同時又有一個事務B,先持有鎖B再去獲得鎖A的時候就會發生死鎖。

4.1死鎖產生的4個必要條件

●互斥條件:指程序對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個程序佔用。如果此時還有其它程序請求資源,則請求者只能等待,直至佔有資源的程序用畢釋放。
●請求和保持條件:指程序已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程序佔有,此時請求程序阻塞,但又對自己已獲得的其它資源保持不放。
●不剝奪條件:指程序已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
●環路等待條件:指在發生死鎖時,必然存在一個程序——資源的環形鏈,即程序集合{P0,P1,P2,•••,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。

4.2死鎖示例

演示還是使用goods.members會員表,MemberName會員名稱欄位為非聚集索引列,清空之前示例資料:

TRUNCATE TABLE goods.members;

預先插入兩條會員資料:

INSERT INTO goods.members (MemberName,Tel) VALUES ('A','110'),('C','130');


事務T1:

UPDATE goods.members SET Tel='130' WHERE MemberName='C';


●記錄鎖:因為MemberName欄位是索引,所以該Update語句肯定會加上MemberName='C'的記錄鎖。
●間隙鎖:Update語句會在非唯一索引的MemberName='C'加上左區間的間隙鎖(A,C)和右區間的間隙鎖(C, +∞)(因為目前goods.members會員表中只有MemberName='C'的一條記錄,所以沒有中間的間隙鎖)。
●Next-Key鎖:記錄鎖(Record Locks)+間隙鎖(Gap Locks),說明Update語句同時持有(A,C]Next-Key鎖。

事務T2:

UPDATE goods.members SET Tel='110' WHERE MemberName='A';


●記錄鎖:因為MemberName欄位是索引,所以該Update語句肯定會加上MemberName='A'的記錄鎖。
●間隙鎖:Update語句會在非唯一索引的MemberName='A'加上左區間的間隙鎖(-∞,A)(因為目前goods.members會員表中只有MemberName='A'的一條記錄,所以沒有中間的間隙鎖)和右區間的間隙鎖(A,C)。
●Next-Key鎖:記錄鎖(Record Locks)+間隙鎖(Gap Locks),說明Update語句同時持有(-∞,A]Next-Key鎖。

事務T1:

INSERT INTO goods.members (MemberName,Tel) VALUE ('B','120');


首先是阻塞等待,等T2執行完畢才顯示結果!
●間隙鎖:因為插入是MemberName=’B’會員資訊(B在A和C之間),所以需要請求加(A,C)的間隙鎖。
●插入意向鎖(Insert Intention):插入意向鎖是在插入一行記錄操作之前設定的一種間隙鎖,這個鎖釋放了一種插入方式的訊號,即事務T1需要插入意向鎖(A,C)。

事務T2:

INSERT INTO goods.members (MemberName,Tel) VALUE ('D','140');


●間隙鎖:因為插入是MemberName=’D’會員資訊(D在C之後),所以需要請求加(C,+∞)的間隙鎖。
●插入意向鎖(Insert Intention):插入意向鎖是在插入一行記錄操作之前設定的一種間隙鎖,這個鎖釋放了一種插入方式的訊號,即事務T2需要插入意向鎖(C,+∞)。

事務T1:

等T2執行完畢後,事務T1插入MemberName=’B’的語句就會由阻塞變為死鎖!

4.3死鎖分析

上面死鎖示例我再畫了一個表格方便大家更加清晰瞭解死鎖發生過程:

順序編號

事務T1

事務T2

BEGIN;

UPDATE goods.members SET Tel='130' WHERE MemberName='C';

持有鎖:(A,C]Next-Key鎖和(C, +∞)間隙鎖。

BEGIN;

UPDATE goods.members SET Tel='110' WHERE MemberName='A';

持有鎖:(-∞,A]Next-Key鎖和(A,C)間隙鎖。

INSERT INTO goods.members (MemberName,Tel) VALUE ('B','120');

持有鎖:(C, +∞)間隙鎖。

等待鎖:(A,C)插入意向鎖。

INSERT INTO goods.members (MemberName,Tel) VALUE ('D','140');

持有鎖:(A, C)間隙鎖。

等待鎖:(C, +∞)插入意向鎖。

Deadlock found when trying to get lock; try restarting transaction


然後我們再通過以下語句來檢視死鎖日誌具體分析一下:

-- 檢視死鎖日誌
SHOW ENGINE INNODB STATUS;

日誌如下:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2021-08-04 11:39:12 0x7fee8b558700
*** (1) TRANSACTION:
TRANSACTION 271069, ACTIVE 590 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 1123904, OS thread handle 140662933055232, query id 4785256 localhost root update
INSERT INTO goods.members (MemberName,Tel) VALUE ('B','120')

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 57 page no 5 n bits 72 index IX_MemberName of table `goods`.`members` trx id 271069 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 1; hex 43; asc C;;
 1: len 4; hex 80000002; asc     ;;

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 57 page no 5 n bits 72 index IX_MemberName of table `goods`.`members` trx id 271069 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 1; hex 43; asc C;;
 1: len 4; hex 80000002; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 271070, ACTIVE 432 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 1123909, OS thread handle 140662461384448, query id 4785257 localhost root update
INSERT INTO goods.members (MemberName,Tel) VALUE ('D','140')

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 57 page no 5 n bits 72 index IX_MemberName of table `goods`.`members` trx id 271070 lock_mode X locks gap before rec
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 1; hex 43; asc C;;
 1: len 4; hex 80000002; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 57 page no 5 n bits 72 index IX_MemberName of table `goods`.`members` trx id 271070 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;
4.3.1事務T1日誌

●找到最新死鎖日誌記錄,並找到事務T1(271069):

●檢視事務T1日誌執行SQL語句:

INSERT INTO goods.members (MemberName,Tel) VALUE ('B','120');

●檢視事務T1日誌裡持有鎖(HOLDS THE LOCK):索引(index IX_MemberName),物理記錄(PHYSICAL RECORD),間隙區間(未知,+∞)、(未知,C)。

●檢視事務T1日誌正在等待鎖釋放(WAITING FOR THIS LOCK TO BE GRANTED):插入意向鎖(lock_mode X locks gap before rec insert intention waiting),索引上(index IX_MemberName),物理記錄(PHYSICAL RECORD),間隙區間(未知,C)。

4.3.2事務T2日誌

●然後找到事務T2(271070):


●檢視事務T2日誌執行SQL語句:

INSERT INTO goods.members (MemberName,Tel) VALUE ('D','140');

●檢視事務T2日誌裡持有鎖(HOLDS THE LOCK):索引(index IX_MemberName),間隙鎖(lock_mode X locks gap before rec),物理記錄(PHYSICAL RECORD),間隙區間(未知,C)。

●檢視事務T2日誌正在等待鎖釋放(WAITING FOR THIS LOCK TO BE GRANTED):插入意向鎖(lock_mode X locks gap before rec insert intention waiting),索引上(index IX_MemberName),物理記錄(PHYSICAL RECORD),間隙區間(未知,+∞)。

4.3.3檢視日誌總結

●事務T1正在等待的插入意向排他鎖,剛好正在事務T2的懷裡。
●事務T2持有間隙鎖,正在等待插入意向排它鎖。

4.4總結

●事務T1執行完Update MemberName='C'語句,持有(A,C]Next-Key鎖和(C, +∞)間隙鎖。
●事務T2執行完Update MemberName='A'語句,持有(-∞,A]Next-Key鎖和(A,C)間隙鎖。
●事務T1執行Insert MemberName='B'的語句時,因為需要(A,C)插入意向鎖,但是(A,C)在事務T2裡面未釋放,所以T1繼續等待。
●事務T2執行Insert MemberName='D'的語句時,因為需要(C, +∞) 插入意向鎖,但是(C, +∞) 在事務T1裡面未釋放,所以T2繼續等待。
●事務T1持有(C, +∞)間隙鎖,在等待(A,C)的插入意向鎖,事務T2持有(A,C)間隙鎖,在等待(C, +∞)的插入意向鎖,所以形成了死鎖的閉環(間隙鎖與插入意向鎖會衝突的,可以看回行鎖的相容矩陣)。
●事務T1,T2形成了死鎖閉環後,因為InnoDB的底層機制,它會讓其中一個事務讓出資源,讓另外的事務執行成功,這就是為什麼你最後看到了事務T2插入成功,而事務T1的插入最後由阻塞顯示為Deadlock found when trying to get lock; try restarting transaction。
注:查詢鎖資訊(MySQL8.0版本):SELECT * FROM `performance_schema`.data_locks;

參考文獻:
深入淺出MySQL大全