“?bug 是程序員的天敵,同時也是程序員進步的階梯”
前情提要
—
前段時間,同事反饋又一批業(yè)務(wù)數(shù)據(jù)入庫非常慢,而且有些數(shù)據(jù)和合作方數(shù)據(jù)對不上,偶爾會有出入。于是,作為對疑難問題感興趣的我就開始協(xié)助排查。
事故現(xiàn)場
—
經(jīng)過登錄日志平臺排查日志發(fā)現(xiàn):
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException:Deadlock found when trying to get lock
那么這就是入庫慢和數(shù)據(jù)和客戶返回部分數(shù)據(jù)對不上的問題的根源了,死鎖。而且出現(xiàn)的概率非常高。
開始懷疑是不是業(yè)務(wù)庫的事物隔離級別設(shè)置的問題,經(jīng)過和基建部門相關(guān)同事確認,數(shù)據(jù)庫隔離級別是RC。
開始代碼走查
發(fā)現(xiàn)入庫的時候,因為記錄中除了唯一索引字段外,其他字段可能會存在更新,?所以用的是replace into ,且是多線程插入的。那么問題很可能就出現(xiàn)在這里。
replace into先是執(zhí)行insert,發(fā)生duplicate key之后再去更新原有記錄。這意味著從引擎的角度看操作并不是原子性的,有可能會有多個行鎖。
為了簡化業(yè)務(wù)邏輯,說明問題,簡化版表的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)場,三個方法對三條數(shù)據(jù)進行更新:
({"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可以分為以下幾步:?
插入聚集索引主鍵,這步一定成功。
插入二級索引,檢查二級唯一索引idx_uk_b上是否有沖突。若是,則undo步驟1插入的聚集索引記錄,轉(zhuǎn)到步驟3;若否,轉(zhuǎn)到步驟4。
處理沖突。通過idx_uk索引定位沖突行并加鎖,insert新記錄成功后delete沖突行。
直接insert記錄。
死鎖就發(fā)生在步驟3的delete + insert中。
我們知道MySQL在RR隔離級別下引入間隙鎖來解決數(shù)據(jù)記錄的幻讀問題,在RC隔離級別下,通常間隙鎖會消失,降級為記錄鎖,所以在RC隔離級別下能夠提高并發(fā)寫入的性能。
但是在某些特殊場景下,RC隔離級別也會包含間隙鎖。要搞明白這個問題
首先需要知道下面2個知識點:?
-
MySQL在唯一索引上加鎖的原則:
唯一索引上的范圍查詢會訪問到不滿足條件的第一個值為止
這個加鎖原則看似不太合理,像一個bug,因為唯一索引意味著所有的記錄不能重復(fù),理論上只需要添加記錄的行鎖就可以,但是實際中確實是需要訪問到當前記錄的下一條記錄進行加鎖。
-
插入意向鎖
插入意向鎖之間是不沖突的,插入意向鎖也是一種間隙鎖,他的存在是為了提高插入的并發(fā)度。在申請插入意向鎖的時候,需要判斷當前插入記錄位置的下一條記錄上是否持有鎖,如果有,則需要判斷是否與插入意向鎖沖突。如果沒有,則不需要判斷,直接加上插入意向鎖(Next-Key Lock)。
而且根據(jù)MySQL 5.7 Reference,在delete一行時Next-Key Lock會將該行在索引上的前一個區(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.
如果兩個session同時需要delete同一個主鍵的記錄【要知道,唯一索引也是一顆B+樹,這個B+樹的主鍵就是字段b】,并insert一條B+樹索引排序在刪除數(shù)據(jù)的前面的記錄(例如:insert一條主鍵更小的記錄),死鎖就有可能發(fā)生。
結(jié)論
—
現(xiàn)在我們知道,問題關(guān)鍵點在于replace是以delete + insert的方式去更新記錄,改變了聚集索引上的值,更新后自增主鍵id 是不一樣的。解決這個問題的方案是,更新的時候避免重新分配新的記錄,具體可以使用insert ... on duplicate key update ...。該SQL遇到唯一索引沖突時,總是使用update舊記錄的方式來更新。
insert into c values(NULL,4) on duplicate key update b=4 這樣問題就解決了,當然,也給下次海量數(shù)據(jù)分頁留下了伏筆。
我是小汪哥,希望能收到你的鍵盤傳遞的電流!
本文使用 文章同步助手 同步