一個Bug引發(fā)的死鎖
最近有用戶反映,系統(tǒng)時不時會出現(xiàn)報錯的現(xiàn)象。登陸生產(chǎn)環(huán)境查看日志,發(fā)現(xiàn)MySQL出現(xiàn)了死鎖。根據(jù)報錯信息排查,在生產(chǎn)環(huán)境發(fā)現(xiàn)了如下代碼。
session.query(TblMsg.id).filter(TblMsg.key == key).with_for_update()
obj_msg = TblMsg(
group_id=group_id,
key=key,
sep=sep
)
session.add(obj_msg)
session.commit()
為了使邏輯更加清晰,代碼我已經(jīng)簡化過了。經(jīng)常運行到會話提交事務時,就產(chǎn)生死鎖。下面我們就來一步步分析,產(chǎn)生死鎖的原因以及解決策略。
InnoDB行鎖的種類
首先,我們來看看InnoDB中行鎖的種類。
Recodrd Lock: 行鎖,每行級別加鎖。
Gap Lock: 間隙鎖,鎖住一個范圍,但并不包括要鎖的那個值。
Next key Lock: 行鎖和間隙鎖的結合,即鎖住范圍也鎖住要鎖的值,是Gap Lock和Next key Lock的結合。
事務的隔離級別
提及到鎖,那么就一定離不開事務的隔離級別。

這里我們著重關注READ COMMITED和REPEATABLE READ,MySQL默認的事務隔離級別為REPEATABLE READ。我們都知道在REPEATABLE READ這個隔離級別下,理論上是會出現(xiàn)幻讀的。為了解決這個問題InnoDB引入了間隙鎖這個概念。
Gap Lock
Gap Lock在RR隔離級別上才會生效,其他事務隔離級別不會出現(xiàn)Gap Lock。間隙鎖的出現(xiàn)還和索引有關系。列如update xxx set a=1 where a=2;這個語句。

現(xiàn)在主要來看看a為輔助索引的情況,借此來研究Gap Lock的技術細節(jié)。首先我們創(chuàng)建一張表。
create table `test_gap_lock`(
id unsigned int primary key,
number int,
key `key_number` ("number"),
);
然后在db中插入數(shù)據(jù)
INSERT INTO `test_gap_lock`(
`id`, `number`
)VALUE
(1,2),
(3,4),
(6,5),
(8,5),
(10,5),
(13,11);
在輔助索引a上存在的next-key lock為(-∞, 2], (2, 4], (4, 5], (5, 11],(11, +∞)
現(xiàn)在我們啟動兩個session
# session 1
BEGIN ;
SELECT * from `test_gap_lock` WHERE number=4 FOR UPDATE ;
# session 2
INSERT INTO `test_gap_lock`(`id`, `number`) VALUE (100, 3); # 阻塞
INSERT INTO `test_gap_lock`(`id`, `number`) VALUE (5, 5); #阻塞
INSERT INTO `test_gap_lock`(`id`, `number`) VALUE (7, 5); #成功
現(xiàn)在來解釋一下,因為number為輔助索引,現(xiàn)在手動給number為4的值加一把寫鎖時,會鎖住4附近的間隙。即(2, 4]和(4, 5]這個區(qū)間。插入number值為3的行,在這個范圍內(nèi),所以會阻塞住。
二、三次插入同樣的值,第二次插入阻塞,第三次插入成功,則和索引的排列有關。在輔助索引中,如果索引上的值相同,那么則按照聚集索引的順序進行排列。因為id=5的這次插入在id=3和id=6這兩行數(shù)據(jù)之間,所以被阻塞住。而id=7的插入,不在這個范圍內(nèi),所以能插入成功。
解決問題
有了上面的知識,這個Bug的產(chǎn)生的原因就顯而易見了。在并發(fā)的條件下,sql的執(zhí)行順序可能產(chǎn)生一下的情況。
# session 1
session.query(TblMsg.id).filter(TblMsg.key == key).with_for_update()
# session 2
session.query(TblMsg.id).filter(TblMsg.key == key).with_for_update()
# session 1 阻塞
session.add(obj_msg)
session.commit()
# session 2 阻塞
session.add(obj_msg)
session.commit()
key為表上的一個二級索引,當手動加鎖的時候,鎖的性質(zhì)變?yōu)镹EXT-KEY Lock。不僅鎖住key值,同時也鎖住間隙。session 1鎖住了相應的間隙,session 2也鎖住了相應的間隙。如果這個時候session 1鎖住的間隙,正好是session 2要插入的值。session 2鎖住的間隙,是session 1要插入的值。就會出現(xiàn)死鎖。
解決問題的辦法很簡單,有兩種策略:
1、 使用Unique key或主鍵作為篩選條件,從next-key lock退化為recode lock。
2、 事務的隔離級別從RR退回到RC,或者手動設置參數(shù)關閉gap lock。這種更改最為簡單,但可能出現(xiàn)幻讀,所以需要確定幻讀不會影響業(yè)務的正常運行。