死锁:多线程同时删除唯一索引上的同一行
1????死鎖問題背景????1
1.1????一個不可思議的死鎖????1
1.1.1????初步分析????3
1.2????如何閱讀死鎖日志????3
2????死鎖原因深入剖析????4
2.1????Delete操作的加鎖邏輯????4
2.2????死鎖預(yù)防策略????5
2.3????剖析死鎖的成因????6
3????總結(jié)????7
?
?
死鎖問題背景
?
做MySQL代碼的深入分析也有些年頭了,再加上自己10年左右的數(shù)據(jù)庫內(nèi)核研發(fā)經(jīng)驗,自認(rèn)為對于MySQL/InnoDB的加鎖實現(xiàn)了如指掌,正因如此,前段時間,還專門寫了一篇洋洋灑灑的文章,專門分析MySQL的加鎖實現(xiàn)細節(jié):《MySQL加鎖處理分析》。
?
但是,昨天”潤潔”同學(xué)在《MySQL加鎖處理分析》這篇博文下咨詢的一個MySQL的死鎖場景,還是徹底把我給難住了。此死鎖,完全違背了本人原有的鎖知識體系,讓我百思不得其解。本著機器不會騙人,既然報出死鎖,那么就一定存在死鎖的原則,我又重新深入分析了InnoDB對應(yīng)的源碼實現(xiàn),進行多次實驗,配合恰到好處的靈光一現(xiàn),還真讓我分析出了這個死鎖產(chǎn)生的原因。這篇博文的余下部分的內(nèi)容安排,首先是給出”潤潔”同學(xué)描述的死鎖場景,然后再給出我的剖析。對個人來說,這是一篇十分有必要的總結(jié),對此博文的讀者來說,希望以后碰到類似的死鎖問題時,能夠明確死鎖的原因所在。
?
?
一個不可思議的死鎖
?
“潤潔”同學(xué),給出的死鎖場景如下:
?
表結(jié)構(gòu):
?
CREATE TABLE dltask (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT ‘a(chǎn)uto 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列。
?
事務(wù)隔離級別:
?
RR (Repeatable Read)
?
每個事務(wù)只有一條SQL:
?
delete from dltask where a=? and b=? and c=?;
?
SQL的執(zhí)行計劃:
?
?
死鎖日志:
?
?
初步分析
?
并發(fā)事務(wù),每個事務(wù)只有一條SQL語句:給定唯一的二級索引鍵值,刪除一條記錄。每個事務(wù),最多只會刪除一條記錄,為什么會產(chǎn)生死鎖?這絕對是不可能的。但是,事實上,卻真的是發(fā)生了死鎖。產(chǎn)生死鎖的兩個事務(wù),刪除的是同一條記錄,這應(yīng)該是死鎖發(fā)生的一個潛在原因,但是,即使是刪除同一條記錄,從原理上來說,也不應(yīng)該產(chǎn)生死鎖。因此,經(jīng)過初步分析,這個死鎖是不可能產(chǎn)生的。這個結(jié)論,遠遠不夠!
?
如何閱讀死鎖日志
?
在詳細給出此死鎖產(chǎn)生的原因之前,讓我們先來看看,如何閱讀MySQL給出的死鎖日志。
?
以上打印出來的死鎖日志,由InnoDB引擎中的lock0lock.c::lock_deadlock_recursive()函數(shù)產(chǎn)生。死鎖中的事務(wù)信息,通過調(diào)用函數(shù)lock_deadlock_trx_print()處理;而每個事務(wù)持有、等待的鎖信息,由lock_deadlock_lock_print()函數(shù)產(chǎn)生。
?
例如,以上的死鎖,有兩個事務(wù)。事務(wù)1,當(dāng)前正在操作一張表(mysql tables in use 1),持有兩把鎖(2 lock structs,一個表級意向鎖,一個行鎖(1 row lock)),這個事務(wù),當(dāng)前正在處理的語句是一條delete語句。同時,這唯一的一個行鎖,處于等待狀態(tài)(WAITING FOR THIS LOCK TO BE GRANTED)。
?
事務(wù)1等待中的行鎖,加鎖的對象是唯一索引uniq_a_b_c上頁面號為12713頁面上的一行(注:具體是哪一行,無法看到。但是能夠看到的是,這個行鎖,一共有96個bits可以用來鎖96個行記錄,n bits 96:lock_rec_print()方法)。同時,等待的行鎖模式為next key鎖(lock_mode X)。(注:關(guān)于InnoDB的鎖模式,可參考我早期的一篇PPT:《InnoDB 事務(wù)/鎖/多版本 實現(xiàn)分析》。簡單來說,next key鎖有兩層含義,一是對當(dāng)前記錄加X鎖,防止記錄被并發(fā)修改,同時鎖住記錄之前的GAP,防止有新的記錄插入到此記錄之前。)
?
同理,可以分析事務(wù)2。事務(wù)2上有兩個行鎖,兩個行鎖對應(yīng)的也都是唯一索引uniq_a_b_c上頁面號為12713頁面上的某一條記錄。一把行鎖處于持有狀態(tài),鎖模式為X lock with no gap(注:記錄鎖,只鎖記錄,但是不鎖記錄前的GAP,no gap lock)。一把行鎖處于等待狀態(tài),鎖模式為next key鎖(注:與事務(wù)1等待的鎖模式一致。同時,需要注意的一點是,事務(wù)2的兩個鎖模式,并不是一致的,不完全相容。持有的鎖模式為X lock with no gap,等待的鎖模式為next key lock X。因此,并不能因為持有了X lock with no gap,就可以說next key lock X就一定能夠加上。)。
?
分析這個死鎖日志,就能發(fā)現(xiàn)一個死鎖。事務(wù)1的next key lock X正在等待事務(wù)2持有的X lock with no gap(行鎖X沖突),同時,事務(wù)2的next key lock X,卻又在等待事務(wù)1正在等待中的next key鎖(注:這里,事務(wù)2等待事務(wù)1的原因,在于公平競爭,杜絕事務(wù)1發(fā)生饑餓現(xiàn)象。),形成循環(huán)等待,死鎖產(chǎn)生。
?
死鎖產(chǎn)生后,根據(jù)兩個事務(wù)的權(quán)重,事務(wù)1的權(quán)重更小,被選為死鎖的犧牲者,回滾。
?
根據(jù)對于死鎖日志的分析,確認(rèn)死鎖確實存在。而且,產(chǎn)生死鎖的兩個事務(wù),確實都是在運行同樣的基于唯一索引的等值刪除操作。既然死鎖確實存在,那么接下來,就是抓出這個死鎖產(chǎn)生原因。
?
死鎖原因深入剖析
?
Delete操作的加鎖邏輯
?
在《MySQL加鎖處理分析》一文中,我詳細分析了各種SQL語句對應(yīng)的加鎖邏輯。例如:Delete語句,內(nèi)部就包含一個當(dāng)前讀(加鎖讀),然后通過當(dāng)前讀返回的記錄,調(diào)用Delete操作進行刪除。在此文的?組合六:id唯一索引+RR?中,可以看到,RR隔離級別下,針對于滿足條件的查詢記錄,會對記錄加上排它鎖(X鎖),但是并不會鎖住記錄之前的GAP(no gap lock)。對應(yīng)到此文上面的死鎖例子,事務(wù)2所持有的鎖,是一把記錄上的排它鎖,但是沒有鎖住記錄前的GAP(lock_mode X locks rec but not gap),與我之前的加鎖分析一致。
?
其實,在《MySQL加鎖處理分析》一文中的?組合七:id非唯一索引+RR?部分的最后,我還提出了一個問題:如果組合五、組合六下,針對SQL:select * from t1 where id = 10 for update; 第一次查詢,沒有找到滿足查詢條件的記錄,那么GAP鎖是否還能夠省略?針對此問題,參與的朋友在做過試驗之后,給出的正確答案是:此時GAP鎖不能省略,會在第一個不滿足查詢條件的記錄上加GAP鎖,防止新的滿足條件的記錄插入。
?
其實,以上兩個加鎖策略,都是正確的。以上兩個策略,分別對應(yīng)的是:1)唯一索引上滿足查詢條件的記錄存在并且有效;2)唯一索引上滿足查詢條件的記錄不存在。但是,除了這兩個之外,其實還有第三種:3)唯一索引上滿足查詢條件的記錄存在但是無效。眾所周知,InnoDB上刪除一條記錄,并不是真正意義上的物理刪除,而是將記錄標(biāo)識為刪除狀態(tài)。(注:這些標(biāo)識為刪除狀態(tài)的記錄,后續(xù)會由后臺的Purge操作進行回收,物理刪除。但是,刪除狀態(tài)的記錄會在索引中存放一段時間。) 在RR隔離級別下,唯一索引上滿足查詢條件,但是卻是刪除記錄,如何加鎖?InnoDB在此處的處理策略與前兩種策略均不相同,或者說是前兩種策略的組合:對于滿足條件的刪除記錄,InnoDB會在記錄上加next key lock X(對記錄本身加X鎖,同時鎖住記錄前的GAP,防止新的滿足條件的記錄插入。) Unique查詢,三種情況,對應(yīng)三種加鎖策略,總結(jié)如下:
?
- 找到滿足條件的記錄,并且記錄有效,則對記錄加X鎖,No Gap鎖(lock_mode X locks rec but not gap);
- 找到滿足條件的記錄,但是記錄無效(標(biāo)識為刪除的記錄),則對記錄加next key鎖(同時鎖住記錄本身,以及記錄之前的Gap:lock_mode X);
- 未找到滿足條件的記錄,則對第一個不滿足條件的記錄加Gap鎖,保證沒有滿足條件的記錄插入(locks gap before rec);
?
此處,我們看到了next key鎖,是否很眼熟?對了,前面死鎖中事務(wù)1,事務(wù)2處于等待狀態(tài)的鎖,均為next key鎖。明白了這三個加鎖策略,其實構(gòu)造一定的并發(fā)場景,死鎖的原因已經(jīng)呼之欲出。但是,還有一個前提策略需要介紹,那就是InnoDB內(nèi)部采用的死鎖預(yù)防策略。
?
死鎖預(yù)防策略
?
InnoDB引擎內(nèi)部(或者說是所有的數(shù)據(jù)庫內(nèi)部),有多種鎖類型:事務(wù)鎖(行鎖、表鎖),Mutex(保護內(nèi)部的共享變量操作)、RWLock(又稱之為Latch,保護內(nèi)部的頁面讀取與修改)。
?
InnoDB每個頁面為16K,讀取一個頁面時,需要對頁面加S鎖,更新一個頁面時,需要對頁面加上X鎖。任何情況下,操作一個頁面,都會對頁面加鎖,頁面鎖加上之后,頁面內(nèi)存儲的索引記錄才不會被并發(fā)修改。
?
因此,為了修改一條記錄,InnoDB內(nèi)部如何處理:
?
?
剖析死鎖的成因
?
做了這么多鋪墊,有了Delete操作的3種加鎖邏輯、InnoDB的死鎖預(yù)防策略等準(zhǔn)備知識之后,再回過頭來分析本文最初提到的死鎖問題,就會手到拈來,事半而功倍。
?
首先,假設(shè)dltask中只有一條記錄:(1, ‘a(chǎn)’, ‘b’, ‘c’, ‘data’)。三個并發(fā)事務(wù),同時執(zhí)行以下的這條SQL:
?
delete from dltask where a=’a’ and b=’b’ and c=’c’;
?
并且產(chǎn)生了以下的并發(fā)執(zhí)行邏輯,就會產(chǎn)生死鎖:
?
?
上面分析的這個并發(fā)流程,完整展現(xiàn)了死鎖日志中的死鎖產(chǎn)生的原因。其實,根據(jù)事務(wù)1步驟6,與事務(wù)0步驟3/4之間的順序不同,死鎖日志中還有可能產(chǎn)生另外一種情況,那就是事務(wù)1等待的鎖模式為記錄上的X鎖 + No Gap鎖(lock_mode X locks rec but not gap waiting)。這第二種情況,也是”潤潔”同學(xué)給出的死鎖用例中,使用MySQL 5.6.15版本測試出來的死鎖產(chǎn)生的原因。
?
總結(jié)
?
行文至此,MySQL基于唯一索引的單條記錄的刪除操作并發(fā),也會產(chǎn)生死鎖的原因,已經(jīng)分析完畢。其實,分析此死鎖的難點,在于理解MySQL/InnoDB的行鎖模式,針對不同情況下的加鎖模式的區(qū)別,以及InnoDB處理頁面鎖與事務(wù)鎖的死鎖預(yù)防策略。明白了這些,死鎖的分析就會顯得清晰明了。
?
最后,總結(jié)下此類死鎖,產(chǎn)生的幾個前提:
?
- Delete操作,針對的是唯一索引上的等值查詢的刪除;(范圍下的刪除,也會產(chǎn)生死鎖,但是死鎖的場景,跟本文分析的場景,有所不同)
- 至少有3個(或以上)的并發(fā)刪除操作;
- 并發(fā)刪除操作,有可能刪除到同一條記錄,并且保證刪除的記錄一定存在;
- 事務(wù)的隔離級別設(shè)置為Repeatable Read,同時未設(shè)置innodb_locks_unsafe_for_binlog參數(shù)(此參數(shù)默認(rèn)為FALSE);(Read Committed隔離級別,由于不會加Gap鎖,不會有next key,因此也不會產(chǎn)生死鎖)
- 使用的是InnoDB存儲引擎;(廢話!MyISAM引擎根本就沒有行鎖)
總結(jié)
以上是生活随笔為你收集整理的死锁:多线程同时删除唯一索引上的同一行的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 详解 Too many open fil
- 下一篇: 事件查看器ID 1041