(給ImportNew加星標,提高Java技能)
問題背景
我們在開啟多線程對數據庫進行操作的時候,先批量對數據進行刪除,然後再新增,本來想着是考慮到不走更新,性能會提升,但是執行的時候發現報錯,執行的sql等待超時,阻塞了進程,dbcp連接池被打滿,數據庫表訪問不可用。針對這個問題,我們進行了深入的挖掘,逐漸解開了問題的真相。
問題復現復現準備工作
業務問題這裡就不細講了,為了方便講解,操作直接簡化。
數據庫表a有唯⼀索引t1,業務字段t2,主鍵id。數據庫表a的定義如下:
接下來復現問題操作

不走更新操作,先刪除,後插入,保證只有2次數據庫操作。
從業務的⻆度考慮,表中只有id,t1,t2字段,且客戶端實際也只關⼼t2字段。因此決定先根據A的t1字段去數據庫⾥將對應的值刪全部除,然後進⾏批量插⼊操作,這樣的話,⽆論更新還是插⼊,都只有兩次數據庫操作,提高了性能。為了避免插⼊失敗,導致數據被刪除,在handle⽅法上添加事務。
復現報錯
Deadlock found when trying to get lock; try restarting transaction; nested exception is
com.ibatis.common.jdbc.exception.NestedSQLException...
問題原因
查詢相關資料得知,引起死鎖的原因是MYSQL的間隙鎖。
間隙鎖
間隙鎖(Gap Lock)是Innodb在可重複讀提交下為了解決幻讀問題時引⼊的鎖機制,幻讀的問題存在是因為新增或者更新操作,這時如果進⾏範圍查詢的時候(加鎖查詢),會出現不⼀致的問題,這時使⽤不同的⾏鎖已經沒有辦法滿⾜要求,需要對⼀定範圍內的數據進⾏加鎖,間隙鎖就是解決這類問題的。在可重複讀隔離級別下,數據庫是通過⾏鎖和間隙鎖共同組成的(next-key lock)來實現的。
record lock:⾏鎖,也就是僅僅鎖着單獨的⼀⾏。
gap lock:間隙鎖,僅僅鎖住⼀個區間(注意這⾥的區間都是開區間,也就是不包括邊界值)。
next-key lock:record lock+gap lock,所以next-key lock也就半開半閉區間,且是下界開,上界閉。
加鎖規則特性
加鎖的基本單位是(next-key lock),他是前開後閉原則
間隙鎖僅阻⽌其他事務插⼊間隙。在刪除數據的時候,會去加間隙鎖,但是多個事務是可以同時對⼀個間隙去加鎖的,⽽如果需要對該間隙進⾏插⼊,則需要等待鎖釋放。
間隙鎖復現死鎖
step1- ⾸先插⼊測試數據
表a數據初始化後,這時候那麼可能的next-key lock的包括:
(⽆窮⼩, 10],(10,11],(11,13],(13,20],(20, ⽆窮⼤)
step2- 我們開啟兩個窗⼝去模擬死鎖。
此時,Session 1和Session 2都會對區間(20, ⽆窮⼤)加鎖, 因為間隙鎖只是⽤來防⽌其他事務在區間中插⼊數據。
step3- Session1繼續插⼊操作:
此時Session1阻塞(因為Session2持有間隙鎖)。
step4- 緊接着Session2繼續插⼊操作:
此時Session2死鎖,因為Session1持有間隙鎖。⽽我們的代碼⾥⾯,因為涉及到多線程在事務⾥進⾏先刪除後插⼊的操作,就會發⽣死鎖。
解決方式1、將事務隔離級別將為read commit.
間隙鎖只存在於可重複讀的隔離級別下,因為要防⽌幻讀。這個⽅法不現實,不可能為了這個問題 把整個線上數據庫隔離級別給改掉。
2、避免先刪除後插⼊的操作.
修改代碼,避免先刪除後插⼊的操作。犧牲性能,在業務中,先根據唯⼀索引查出存在的記錄,然後對存在的記錄進⾏根據主鍵Id在循環中更新,對於不存在的記錄進⾏批量插⼊。