一.業(yè)務(wù)背景
我們現(xiàn)在的業(yè)務(wù)是一款數(shù)據(jù)產(chǎn)品,有不少實(shí)時計算和爬取來的數(shù)據(jù)都匯總到大數(shù)據(jù)倉庫、數(shù)據(jù)挖掘平臺ODPS上。然后應(yīng)用在讀取這些數(shù)據(jù)時,這些數(shù)據(jù)會先導(dǎo)入到并發(fā)讀能力更強(qiáng),適合結(jié)構(gòu)查詢的MySQL上。數(shù)據(jù)端開發(fā)的同學(xué)在跑定時任務(wù)時, tps比較高,于是出現(xiàn)了一些線上問題:在開發(fā)過程中發(fā)現(xiàn)對某一包含unique key(聯(lián)合的唯一索引)的表進(jìn)行并發(fā)插入的時候,出現(xiàn)大量的死鎖,使得插入幾乎無法進(jìn)行。于是為了排查問題,請教了DBA以及數(shù)據(jù)庫事業(yè)部的同學(xué),最后發(fā)現(xiàn)了問題的所在,特此記錄下來
二.死鎖現(xiàn)場
1.表結(jié)構(gòu)
CREATE TABLE tkn_tb_cinema_show_data (
……
cinema_id bigint(20) DEFAULT NULL COMMENT ‘影院ID’,
show_id bigint(20) DEFAULT NULL COMMENT ‘影片ID’,
now_date varchar(32) DEFAULT NULL COMMENT ‘當(dāng)日時間’,
……
PRIMARY KEY (id),
UNIQUE KEY uid_cinema_show_date (cinema_id,show_id,now_date),
……
) ENGINE=InnoDB AUTO_INCREMENT=2162973490 DEFAULT CHARSET=utf8 COMMENT=’淘寶電影訂單影院影片數(shù)據(jù)統(tǒng)計’
2.問題狀況
可以看到出現(xiàn)死鎖的原因是因?yàn)榕坎迦氲臅r候,該事務(wù)
持有鎖
index uid_cinema_show_date of table tkn.tkn_tb_cinema_show_data trx id 73278630826 lock_mode X
等待鎖
index uid_cinema_show_date of table tkn.tkn_tb_cinema_show_data trx id 73278630826 lock_mode X locks gap before rec insert intention waiting
這樣一看確實(shí)奇怪,怎么批量插入不同行怎么會有死鎖,再看看死鎖日志
(SHOW ENGINE INNODB STATUS;)
transactions deadlock detected, dumping detailed information.
2017-06-11 08:41:03 2ac742684700
*** (1) TRANSACTION:
TRANSACTION 73278630816, ACTIVE 1 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 622 lock struct(s), heap size 79400, 743 row lock(s), undo log entries 388
MySQL thread id 13824253, OS thread handle 0x2ac195786700, query id 53621728233 11.227.64.76 dwexp update
INSERT INTO tkn_tb_cinema_show_data ......
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 29500 page no 398903 n bits 336 index `uid_cinema_show_date` of table `tkn`.`tkn_tb_cinema_show_data` trx id 73278630816 lock_mode X
Record lock, heap no 253 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 8; hex 80000000000035cb; asc 5 ;;
1: len 8; hex 80000000000356f1; asc V ;;
2: len 10; hex 323031372d30362d3138; asc 2017-06-18;;
3: len 8; hex 80000000807f52b2; asc R ;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 29500 page no 423377 n bits 304 index `uid_cinema_show_date` of table `tkn`.`tkn_tb_cinema_show_data` trx id 73278630816 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 228 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 8; hex 8000000000006262; asc bb;;
1: len 8; hex 8000000000035911; asc Y ;;
2: len 10; hex 323031372d30362d3138; asc 2017-06-18;;
3: len 8; hex 80000000807fdae4; asc ;;
*** (2) TRANSACTION:
TRANSACTION 73278630826, ACTIVE 1 sec inserting, thread declared inside InnoDB 4836
mysql tables in use 1, locked 1
2425 lock struct(s), heap size 292392, 3363 row lock(s), undo log entries 1804
MySQL thread id 13824252, OS thread handle 0x2ac742684700, query id 53621728249 11.227.64.76 dwexp update
INSERT INTO tkn_tb_cinema_show_data ......
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 29500 page no 423377 n bits 304 index `uid_cinema_show_date` of table `tkn`.`tkn_tb_cinema_show_data` trx id 73278630826 lock_mode X
Record lock, heap no 228 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 8; hex 8000000000006262; asc bb;;
1: len 8; hex 8000000000035911; asc Y ;;
2: len 10; hex 323031372d30362d3138; asc 2017-06-18;;
3: len 8; hex 80000000807fdae4; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 29500 page no 398903 n bits 336 index `uid_cinema_show_date` of table `tkn`.`tkn_tb_cinema_show_data` trx id 73278630826 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 253 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 8; hex 80000000000035cb; asc 5 ;;
1: len 8; hex 80000000000356f1; asc V ;;
2: len 10; hex 323031372d30362d3138; asc 2017-06-18;;
3: len 8; hex 80000000807f52b2; asc R ;;
*** WE ROLL BACK TRANSACTION (1)
三.分析問題
1.閱讀死鎖日志
1. 從日志中可以看到兩個事務(wù)的持鎖情況和等待鎖情況:
a. 事務(wù)一
HOLDS THE LOCK(S) …… lock_mode X 持有X鎖
WAITING FOR THIS LOCK TO BE GRANTED …… X locks gap before rec insert intention waiting 等待insert intention lock
b.事務(wù)二
HOLDS THE LOCK(S) …… lock_mode X 持有X鎖
WAITING FOR THIS LOCK TO BE GRANTED …… X locks gap before rec insert intention waiting 等待insert intention lock
2. 補(bǔ)充關(guān)于一些鎖方面的知識
當(dāng)InnoDB在判斷行鎖是否沖突的時候,除了最基本的IS IX S X鎖的沖突判斷意外,還有一套更精確的判斷邏輯。除了上面說到的鎖類型,InnoDB還將鎖細(xì)分為如下幾種子類型:
record lock(RK)
鎖直接加在索引記錄上面,鎖住的是key
gap lock(GK)
間隙鎖,鎖定一個范圍,但不包括記錄本身。GAP鎖的目的,是為了防止同一事務(wù)的兩次當(dāng)前讀,出現(xiàn)幻讀的情況
next key lock(NK)
行鎖和間隙鎖組合起來就叫Next-Key Lock
insert intention lock(IK)
如果插入前,該間隙已經(jīng)由gap鎖,那么Insert會申請插入意向鎖。因?yàn)榱吮苊饣米x,當(dāng)其他事務(wù)持有該間隙的間隔鎖,插入意向鎖就會被阻塞(不用直接用gap鎖,是因?yàn)間ap鎖不互斥)。
下面畫的就是“精確模式”鎖兼容矩陣
列相加行已有RKGKIKNK
RK0110
GK1111
IK1010
NK0110
insert中對唯一索引的加鎖邏輯
先做UK沖突檢測,如果存在目標(biāo)行,先對目標(biāo)行加S NK(S lock中的next key lock,下同),這個鎖如果最終插入成功(該記錄在等待期間被其他事務(wù)刪除,此鎖被同時刪除)
如果1成功,對對應(yīng)行加X IK
如果2成功,插入記錄,并對記錄加X RK(有可能是隱式鎖)
3.鎖的細(xì)節(jié)
1. 前文已分析,一個insert SQL需要加的鎖依次為 S NK, X IK, X RK、那么加XIK前需要GK或NK。而insert不需要加GK,因此兩個事務(wù)X IK被申請等待的原因是在申請S NK的過程受到阻塞了。
2. insert完成之后,只會殘留X RK鎖,這就是兩個事務(wù)都有X RK的原因,說明它們剛插入完某幾條記錄。
3. 由1,2可以推測,死鎖是事務(wù)1 的S NK被事務(wù)2的 X RK所阻塞,說明事務(wù)2插入的記錄在事務(wù)1 S NK的范圍內(nèi)。而事務(wù)2的 S NK被 事務(wù)1 阻塞的申請S NK給阻塞,說明事務(wù)1 S NK的范圍要大于事務(wù)2 S NK的范圍。
4. 由第3點(diǎn)推斷,可以證明出事務(wù)2所有的記錄范圍 REC2 是要在 事務(wù)1所有的記錄范圍 REC1之后的,既REC2 < REC1
而插入的業(yè)務(wù)場景的數(shù)據(jù)是:
事務(wù)1
('10076','150686','2017-06-11 08:39:15.866') ,
('10111','150686','2017-06-11 08:39:15.866') ,
('10133','214563','2017-06-11 08:39:15.866') ,
('10171','150686','2017-06-11 08:39:15.866')
事務(wù)2
('15186','150686','2017-06-11 08:39:15.866') ,
('15186','151509','2017-06-11 08:39:15.866') ,
('15186','207522','2017-06-11 08:39:15.866') ,
('15187','151509','2017-06-11 08:39:15.866')
實(shí)際的插入數(shù)據(jù)符合我們的預(yù)期
5.由上面的結(jié)論,我們可以得到一張死鎖循環(huán)圖
四.預(yù)防死鎖
死鎖發(fā)生的條件:
1、資源不能共享,需要只能由一個進(jìn)程或者線程使用
2、請求且保持,已經(jīng)鎖定的資源自給保持著不釋放
3、不剝奪,自給申請到的資源不能被別人剝奪
4、循環(huán)等待
防止死鎖的途徑就是避免滿足死鎖條件的情況發(fā)生,適合這個問題解決的方案有:
1、保持事務(wù)簡短并在一個批處理中
在同一數(shù)據(jù)庫中并發(fā)執(zhí)行多個需要長時間運(yùn)行的事務(wù)時通常發(fā)生死鎖。事務(wù)運(yùn)行時間越長,其持有排它鎖或更新鎖的時間也就越長,從而堵塞了其它活動并可能導(dǎo)致死鎖。保持事務(wù)在一個批處理中,可以最小化事務(wù)的網(wǎng)絡(luò)通信往返量,減少完成事務(wù)可能的延遲并釋放鎖。
2、使用低隔離級別
確定事務(wù)是否能在更低的隔離級別上運(yùn)行。執(zhí)行提交讀允許事務(wù)讀取另一個事務(wù)已讀?。ㄎ葱薷模┑臄?shù)據(jù),而不必等待第一個事務(wù)完成。使用較低的隔離級別(例如提交讀)而不使用較高的隔離級別(例如可串行讀)可以縮短持有共享鎖的時間,從而降低了鎖定爭奪(比如這次的S NK和X IK 是InnoDB引擎Repeatable Read級別才有的)。