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

1.概述

前面章節之因此介紹那麼多鎖的知識點和示例,其實最終目的就是爲了排查與解決死鎖的問題,下面咱們把以前學過鎖知識重溫與補充一遍,而後再經過例子演示下若是排查與解決死鎖。mysql

2.前期準備

●數據庫事務隔離級別sql

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;


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

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;3d

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鎖不兼容。rest

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

相關文章
相關標籤/搜索