一個最難以想象的MySQL死鎖分析

1    死鎖問題背景    1mysql

1.1    一個難以想象的死鎖    1sql

1.1.1    初步分析    3數據庫

1.2    如何閱讀死鎖日誌    3併發

2    死鎖緣由深刻剖析    4函數

2.1    Delete操做的加鎖邏輯    4測試

2.2    死鎖預防策略    5spa

2.3    剖析死鎖的成因    63d

3    總結    7調試

 

 

  1. 死鎖問題背景

 

作MySQL代碼的深刻分析也有些年頭了,再加上本身10年左右的數據庫內核研發經驗,自認爲對於MySQL/InnoDB的加鎖實現瞭如指掌,正因如此,前段時間,還專門寫了一篇洋洋灑灑的文章,專門分析MySQL的加鎖實現細節:《MySQL加鎖處理分析》。日誌

 

可是,昨天」潤潔」同窗在《MySQL加鎖處理分析》這篇博文下諮詢的一個MySQL的死鎖場景,仍是完全把我給難住了。此死鎖,徹底違背了本人原有的鎖知識體系,讓我百思不得其解。本着機器不會騙人,既然報出死鎖,那麼就必定存在死鎖的原則,我又從新深刻分析了InnoDB對應的源碼實現,進行屢次實驗,配合恰到好處的靈光一現,還真讓我分析出了這個死鎖產生的緣由。這篇博文的餘下部分的內容安排,首先是給出」潤潔」同窗描述的死鎖場景,而後再給出個人剖析。對我的來講,這是一篇十分有必要的總結,對此博文的讀者來講,但願之後碰到相似的死鎖問題時,可以明確死鎖的緣由所在。

 

 

 

  1. 一個難以想象的死鎖

 

「潤潔」同窗,給出的死鎖場景以下:

 

表結構:

 

CREATE TABLE dltask (

id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT ‘auto id’,

a varchar(30) NOT NULL COMMENT ‘uniq.a’,

b varchar(30) NOT NULL COMMENT ‘uniq.b’,

c varchar(30) NOT NULL COMMENT ‘uniq.c’,

x varchar(30) NOT NULL COMMENT ‘data’,

PRIMARY KEY (id),

UNIQUE KEY uniq_a_b_c (a, b, c)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=’deadlock test’;

 

a,b,c三列,組合成一個惟一索引,主鍵索引爲id列。

 

事務隔離級別:

 

RR (Repeatable Read)

 

每一個事務只有一條SQL:

 

delete from dltask where a=? and b=? and c=?;

 

SQL的執行計劃:

 

執行計劃

 

死鎖日誌:

 

死鎖日誌

 

  1. 初步分析

 

併發事務,每一個事務只有一條SQL語句:給定惟一的二級索引鍵值,刪除一條記錄。每一個事務,最多隻會刪除一條記錄,爲何會產生死鎖?這絕對是不可能的。可是,事實上,卻真的是發生了死鎖。產生死鎖的兩個事務,刪除的是同一條記錄,這應該是死鎖發生的一個潛在緣由,可是,即便是刪除同一條記錄,從原理上來講,也不該該產生死鎖。所以,通過初步分析,這個死鎖是不可能產生的。這個結論,遠遠不夠!

 

  1. 如何閱讀死鎖日誌

 

在詳細給出此死鎖產生的緣由以前,讓咱們先來看看,如何閱讀MySQL給出的死鎖日誌。

 

以上打印出來的死鎖日誌,由InnoDB引擎中的lock0lock.c::lock_deadlock_recursive()函數產生。死鎖中的事務信息,經過調用函數lock_deadlock_trx_print()處理;而每一個事務持有、等待的鎖信息,由lock_deadlock_lock_print()函數產生。

 

例如,以上的死鎖,有兩個事務。事務1,當前正在操做一張表(mysql tables in use 1),持有兩把鎖(2 lock structs,一個表級意向鎖,一個行鎖(1 row lock)),這個事務,當前正在處理的語句是一條delete語句。同時,這惟一的一個行鎖,處於等待狀態(WAITING FOR THIS LOCK TO BE GRANTED)。

 

事務1等待中的行鎖,加鎖的對象是惟一索引uniq_a_b_c上頁面號爲12713頁面上的一行(注:具體是哪一行,沒法看到。可是可以看到的是,這個行鎖,一共有96個bits能夠用來鎖96個行記錄,n bits 96:lock_rec_print()方法)。同時,等待的行鎖模式爲next key鎖(lock_mode X)。(注:關於InnoDB的鎖模式,可參考我早期的一篇PPT:《InnoDB 事務/鎖/多版本 實現分析》。簡單來講,next key鎖有兩層含義,一是對當前記錄加X鎖,防止記錄被併發修改,同時鎖住記錄以前的GAP,防止有新的記錄插入到此記錄以前。)

 

同理,能夠分析事務2。事務2上有兩個行鎖,兩個行鎖對應的也都是惟一索引uniq_a_b_c上頁面號爲12713頁面上的某一條記錄。一把行鎖處於持有狀態,鎖模式爲X lock with no gap(注:記錄鎖,只鎖記錄,可是不鎖記錄前的GAP,no gap lock)。一把行鎖處於等待狀態,鎖模式爲next key鎖(注:與事務1等待的鎖模式一致。同時,須要注意的一點是,事務2的兩個鎖模式,並非一致的,不徹底相容。持有的鎖模式爲X lock with no gap,等待的鎖模式爲next key lock X。所以,並不能由於持有了X lock with no gap,就能夠說next key lock X就必定可以加上。)。

 

分析這個死鎖日誌,就能發現一個死鎖。事務1的next key lock X正在等待事務2持有的X lock with no gap(行鎖X衝突),同時,事務2的next key lock X,卻又在等待事務1正在等待中的next key鎖(注:這裏,事務2等待事務1的緣由,在於公平競爭,杜絕事務1發生飢餓現象。),造成循環等待,死鎖產生。

 

死鎖產生後,根據兩個事務的權重,事務1的權重更小,被選爲死鎖的犧牲者,回滾。

 

根據對於死鎖日誌的分析,確認死鎖確實存在。並且,產生死鎖的兩個事務,確實都是在運行一樣的基於惟一索引的等值刪除操做。既然死鎖確實存在,那麼接下來,就是抓出這個死鎖產生緣由。

 

  1. 死鎖緣由深刻剖析

 

  1. Delete操做的加鎖邏輯

 

在《MySQL加鎖處理分析》一文中,我詳細分析了各類SQL語句對應的加鎖邏輯。例如:Delete語句,內部就包含一個當前讀(加鎖讀),而後經過當前讀返回的記錄,調用Delete操做進行刪除。在此文的 組合六:id惟一索引+RR 中,能夠看到,RR隔離級別下,針對於知足條件的查詢記錄,會對記錄加上排它鎖(X鎖),可是並不會鎖住記錄以前的GAP(no gap lock)。對應到此文上面的死鎖例子,事務2所持有的鎖,是一把記錄上的排它鎖,可是沒有鎖住記錄前的GAP(lock_mode X locks rec but not gap),與我以前的加鎖分析一致。

 

其實,在《MySQL加鎖處理分析》一文中的 組合七:id非惟一索引+RR 部分的最後,我還提出了一個問題:若是組合5、組合六下,針對SQL:select * from t1 where id = 10 for update; 第一次查詢,沒有找到知足查詢條件的記錄,那麼GAP鎖是否還可以省略?針對此問題,參與的朋友在作過試驗以後,給出的正確答案是:此時GAP鎖不能省略,會在第一個不知足查詢條件的記錄上加GAP鎖,防止新的知足條件的記錄插入。

 

其實,以上兩個加鎖策略,都是正確的。以上兩個策略,分別對應的是:1)惟一索引上知足查詢條件的記錄存在而且有效;2)惟一索引上知足查詢條件的記錄不存在。可是,除了這兩個以外,其實還有第三種:3)惟一索引上知足查詢條件的記錄存在可是無效。衆所周知,InnoDB上刪除一條記錄,並非真正意義上的物理刪除,而是將記錄標識爲刪除狀態。(注:這些標識爲刪除狀態的記錄,後續會由後臺的Purge操做進行回收,物理刪除。可是,刪除狀態的記錄會在索引中存放一段時間。) 在RR隔離級別下,惟一索引上知足查詢條件,可是倒是刪除記錄,如何加鎖?InnoDB在此處的處理策略與前兩種策略均不相同,或者說是前兩種策略的組合:對於知足條件的刪除記錄,InnoDB會在記錄上加next key lock X(對記錄自己加X鎖,同時鎖住記錄前的GAP,防止新的知足條件的記錄插入。) Unique查詢,三種狀況,對應三種加鎖策略,總結以下:

 

  • 找到知足條件的記錄,而且記錄有效,則對記錄加X鎖,No Gap鎖(lock_mode X locks rec but not gap);

     

  • 找到知足條件的記錄,可是記錄無效(標識爲刪除的記錄),則對記錄加next key鎖(同時鎖住記錄自己,以及記錄以前的Gap:lock_mode X);

  • 未找到知足條件的記錄,則對第一個不知足條件的記錄加Gap鎖,保證沒有知足條件的記錄插入(locks gap before rec);

 

此處,咱們看到了next key鎖,是否很眼熟?對了,前面死鎖中事務1,事務2處於等待狀態的鎖,均爲next key鎖。明白了這三個加鎖策略,其實構造必定的併發場景,死鎖的緣由已經呼之欲出。可是,還有一個前提策略須要介紹,那就是InnoDB內部採用的死鎖預防策略。

 

  1. 死鎖預防策略

 

InnoDB引擎內部(或者說是全部的數據庫內部),有多種鎖類型:事務鎖(行鎖、表鎖),Mutex(保護內部的共享變量操做)、RWLock(又稱之爲Latch,保護內部的頁面讀取與修改)。

 

InnoDB每一個頁面爲16K,讀取一個頁面時,須要對頁面加S鎖,更新一個頁面時,須要對頁面加上X鎖。任何狀況下,操做一個頁面,都會對頁面加鎖,頁面鎖加上以後,頁面內存儲的索引記錄纔不會被併發修改。

 

所以,爲了修改一條記錄,InnoDB內部如何處理:

 

  1. 根據給定的查詢條件,找到對應的記錄所在頁面;

     

  2. 對頁面加上X鎖(RWLock),而後在頁面內尋找知足條件的記錄;

     

  3. 在持有頁面鎖的狀況下,對知足條件的記錄加事務鎖(行鎖:根據記錄是否知足查詢條件,記錄是否已經被刪除,分別對應於上面提到的3種加鎖策略之一);

     

  4. 死鎖預防策略:相對於事務鎖,頁面鎖是一個短時間持有的鎖,而事務鎖(行鎖、表鎖)是長期持有的鎖。所以,爲了防止頁面鎖與事務鎖之間產生死鎖。InnoDB作了死鎖預防的策略:持有事務鎖(行鎖、表鎖),能夠等待獲取頁面鎖;但反之,持有頁面鎖,不能等待持有事務鎖。

     

  5. 根據死鎖預防策略,在持有頁面鎖,加行鎖的時候,若是行鎖須要等待。則釋放頁面鎖,而後等待行鎖。此時,行鎖獲取沒有任何鎖保護,所以加上行鎖以後,記錄可能已經被併發修改。所以,此時要從新加回頁面鎖,從新判斷記錄的狀態,從新在頁面鎖的保護下,對記錄加鎖。若是此時記錄未被併發修改,那麼第二次加鎖可以很快完成,由於已經持有了相同模式的鎖。可是,若是記錄已經被併發修改,那麼,就有可能致使本文前面提到的死鎖問題。

  1. 以上的InnoDB死鎖預防處理邏輯,對應的函數,是row0sel.c::row_search_for_mysql()。感興趣的朋友,能夠跟蹤調試下這個函數的處理流程,很複雜,可是集中了InnoDB的精髓。

 

  1. 剖析死鎖的成因

 

作了這麼多鋪墊,有了Delete操做的3種加鎖邏輯、InnoDB的死鎖預防策略等準備知識以後,再回過頭來分析本文最初提到的死鎖問題,就會手到拈來,事半而功倍。

 

首先,假設dltask中只有一條記錄:(1, ‘a’, ‘b’, ‘c’, ‘data’)。三個併發事務,同時執行如下的這條SQL:

 

delete from dltask where a=’a’ and b=’b’ and c=’c’;

 

而且產生了如下的併發執行邏輯,就會產生死鎖:

 

deadlock

 

上面分析的這個併發流程,完整展示了死鎖日誌中的死鎖產生的緣由。其實,根據事務1步驟6,與事務0步驟3/4之間的順序不一樣,死鎖日誌中還有可能產生另一種狀況,那就是事務1等待的鎖模式爲記錄上的X鎖 + No Gap鎖(lock_mode X locks rec but not gap waiting)。這第二種狀況,也是」潤潔」同窗給出的死鎖用例中,使用MySQL 5.6.15版本測試出來的死鎖產生的緣由。

 

  1. 總結

 

行文至此,MySQL基於惟一索引的單條記錄的刪除操做併發,也會產生死鎖的緣由,已經分析完畢。其實,分析此死鎖的難點,在於理解MySQL/InnoDB的行鎖模式,針對不一樣狀況下的加鎖模式的區別,以及InnoDB處理頁面鎖與事務鎖的死鎖預防策略。明白了這些,死鎖的分析就會顯得清晰明瞭。

 

最後,總結下此類死鎖,產生的幾個前提:

 

  • Delete操做,針對的是惟一索引上的等值查詢的刪除;(範圍下的刪除,也會產生死鎖,可是死鎖的場景,跟本文分析的場景,有所不一樣)

     

  • 至少有3個(或以上)的併發刪除操做;

  • 併發刪除操做,有可能刪除到同一條記錄,而且保證刪除的記錄必定存在;

  • 事務的隔離級別設置爲Repeatable Read,同時未設置innodb_locks_unsafe_for_binlog參數(此參數默認爲FALSE);(Read Committed隔離級別,因爲不會加Gap鎖,不會有next key,所以也不會產生死鎖)

  • 使用的是InnoDB存儲引擎;(廢話!MyISAM引擎根本就沒有行鎖)

相關文章
相關標籤/搜索