“?bug 是程序員的天敵,同時(shí)也是程序員進(jìn)步的階梯”
前情提要
—
前段時(shí)間,同事反饋又一批業(yè)務(wù)數(shù)據(jù)入庫(kù)非常慢,而且有些數(shù)據(jù)和合作方數(shù)據(jù)對(duì)不上,偶爾會(huì)有出入。于是,作為對(duì)疑難問題感興趣的我就開始協(xié)助排查。
事故現(xiàn)場(chǎng)
—
經(jīng)過(guò)登錄日志平臺(tái)排查日志發(fā)現(xiàn):
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException:Deadlock found when trying to get lock
那么這就是入庫(kù)慢和數(shù)據(jù)和客戶返回部分?jǐn)?shù)據(jù)對(duì)不上的問題的根源了,死鎖。而且出現(xiàn)的概率非常高。
開始懷疑是不是業(yè)務(wù)庫(kù)的事物隔離級(jí)別設(shè)置的問題,經(jīng)過(guò)和基建部門相關(guān)同事確認(rèn),數(shù)據(jù)庫(kù)隔離級(jí)別是RC。
開始代碼走查
發(fā)現(xiàn)入庫(kù)的時(shí)候,因?yàn)橛涗浿谐宋ㄒ凰饕侄瓮?,其他字段可能?huì)存在更新,?所以用的是replace into ,且是多線程插入的。那么問題很可能就出現(xiàn)在這里。
replace into先是執(zhí)行insert,發(fā)生duplicate key之后再去更新原有記錄。這意味著從引擎的角度看操作并不是原子性的,有可能會(huì)有多個(gè)行鎖。
為了簡(jiǎn)化業(yè)務(wù)邏輯,說(shuō)明問題,簡(jiǎn)化版表的DDL:
其中,字段b是唯一索引鍵。
CREATE TABLE `c` (`a` int(11) NOT NULL AUTO_INCREMENT,??`b`?int(11)?DEFAULT?NULL,PRIMARY KEY (`a`),UNIQUE KEY `idx_uk_b` (`b`)) ENGINE=InnoDB
我們用程序模擬一下 案發(fā)現(xiàn)場(chǎng),三個(gè)方法對(duì)三條數(shù)據(jù)進(jìn)行更新:
({"replace?into?c?values(NULL,3)"})void replaceInsertTest1();({"replace?into?c?values(NULL,1)"})void replaceInsertTest2();({"replace?into?c?values(NULL,2)"})void?replaceInsertTest3();
模擬多線程寫入
new Thread() {@Overridepublic void run() {while (flag) {try {????????????????mapper.replaceInsertTest1();} catch (Exception e) {log.error(e.getMessage(), e);flag = false;}}}}.start();new Thread() {@Overridepublic void run() {while (flag) {????????????try?{????????????????mapper.replaceInsertTest2();} catch (Exception e) {log.error(e.getMessage(), e);flag = false;}}}}.start();new Thread() {@Overridepublic void run() {while (flag) {try {????????????????mapper.replaceInsertTest3();} catch (Exception e) {log.error(e.getMessage(), e);flag = false;}}}}.start();

死鎖出現(xiàn)了。
原因分析
—
發(fā)生duplicate key沖突的索引是idx_uk_b。這種情況下replace into可以分為以下幾步:?
插入聚集索引主鍵,這步一定成功。
插入二級(jí)索引,檢查二級(jí)唯一索引idx_uk_b上是否有沖突。若是,則undo步驟1插入的聚集索引記錄,轉(zhuǎn)到步驟3;若否,轉(zhuǎn)到步驟4。
處理沖突。通過(guò)idx_uk索引定位沖突行并加鎖,insert新記錄成功后delete沖突行。
直接insert記錄。
死鎖就發(fā)生在步驟3的delete + insert中。
我們知道MySQL在RR隔離級(jí)別下引入間隙鎖來(lái)解決數(shù)據(jù)記錄的幻讀問題,在RC隔離級(jí)別下,通常間隙鎖會(huì)消失,降級(jí)為記錄鎖,所以在RC隔離級(jí)別下能夠提高并發(fā)寫入的性能。
但是在某些特殊場(chǎng)景下,RC隔離級(jí)別也會(huì)包含間隙鎖。要搞明白這個(gè)問題
首先需要知道下面2個(gè)知識(shí)點(diǎn):?
-
MySQL在唯一索引上加鎖的原則:
唯一索引上的范圍查詢會(huì)訪問到不滿足條件的第一個(gè)值為止
這個(gè)加鎖原則看似不太合理,像一個(gè)bug,因?yàn)槲ㄒ凰饕馕吨械挠涗洸荒苤貜?fù),理論上只需要添加記錄的行鎖就可以,但是實(shí)際中確實(shí)是需要訪問到當(dāng)前記錄的下一條記錄進(jìn)行加鎖。
-
插入意向鎖
插入意向鎖之間是不沖突的,插入意向鎖也是一種間隙鎖,他的存在是為了提高插入的并發(fā)度。在申請(qǐng)插入意向鎖的時(shí)候,需要判斷當(dāng)前插入記錄位置的下一條記錄上是否持有鎖,如果有,則需要判斷是否與插入意向鎖沖突。如果沒有,則不需要判斷,直接加上插入意向鎖(Next-Key Lock)。
而且根據(jù)MySQL 5.7 Reference,在delete一行時(shí)Next-Key Lock會(huì)將該行在索引上的前一個(gè)區(qū)間鎖住以防止幻讀。
That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.
如果兩個(gè)session同時(shí)需要delete同一個(gè)主鍵的記錄【要知道,唯一索引也是一顆B+樹,這個(gè)B+樹的主鍵就是字段b】,并insert一條B+樹索引排序在刪除數(shù)據(jù)的前面的記錄(例如:insert一條主鍵更小的記錄),死鎖就有可能發(fā)生。
結(jié)論
—
現(xiàn)在我們知道,問題關(guān)鍵點(diǎn)在于replace是以delete + insert的方式去更新記錄,改變了聚集索引上的值,更新后自增主鍵id 是不一樣的。解決這個(gè)問題的方案是,更新的時(shí)候避免重新分配新的記錄,具體可以使用insert ... on duplicate key update ...。該SQL遇到唯一索引沖突時(shí),總是使用update舊記錄的方式來(lái)更新。
insert into c values(NULL,4) on duplicate key update b=4 這樣問題就解決了,當(dāng)然,也給下次海量數(shù)據(jù)分頁(yè)留下了伏筆。
我是小汪哥,希望能收到你的鍵盤傳遞的電流!
本文使用 文章同步助手 同步