(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大全


免責聲明!

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



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