問題背景
我們在開啟多執行緒對資料庫進行操作的時候,先批量對資料進行刪除,然後再新增,本來想著是考慮到不走更新,效能會提升,但是執行的時候發現報錯,執行的sql等待超時,阻塞了程式,dbcp連線池被打滿,資料庫表訪問不可用。針對這個問題,我們進行了深入的挖掘,逐漸解開了問題的真相。
看下具體的業務實現細節
- 表定義
- 現在導⼊⼀批資料A的集合,A的定義如下所示:
接下來複現問題操作
- 根據t1的值查詢表a中有沒有對應的記錄
- 如果有值,則更新t2的值
- 如果沒查到結果,則執行insert插入操作
這裡批量操作我們採用了多執行緒的方式來執行
問題復現
- step1 - ⾸先插⼊測試資料
- step2 - 我們開啟兩個窗⼝去模擬死鎖。
Session1:
Session2:
此時,Session 1和Session 2都會對區間(20, ⽆窮⼤)加鎖, 因為間隙鎖只是⽤來防⽌其他事務在區間中插⼊資料。 - step3 - Session1繼續插⼊操作:
此時Session1阻塞(因為Session2持有間隙鎖)。
- step4- 緊接著Session2繼續插⼊操作:
此時Session2死鎖,因為Session1持有間隙鎖。⽽我們的程式碼⾥⾯,因為涉及到多執行緒在事務⾥進⾏先刪除後插⼊的操作,就會發⽣死鎖。
不走更新操作,先刪除,後插入,保證只有2次資料庫操作。
問題原因
查詢相關資料得知,引起死鎖的原因是MYSQL的間隙鎖。
間隙鎖
間隙鎖(Gap Lock)是Innodb在可重複讀提交下為了解決幻讀問題時引⼊的鎖機制,幻讀的問題存在是因為新增或者更新操作,這時如果進⾏範圍查詢的時候(加鎖查詢),會出現不⼀致的問題,這時使⽤不同的⾏鎖已經沒有辦法滿⾜要求,需要對⼀定範圍內的資料進⾏加鎖,間隙鎖就是解決這類問題的。在可重複讀隔離級別下,資料庫是通過⾏鎖和間隙鎖共同組成的(next-key lock)來實現的。
⾏鎖和間隙鎖的定義如下所示:
- record lock:⾏鎖,也就是僅僅鎖著單獨的⼀⾏。
- gap lock:間隙鎖,僅僅鎖住⼀個區間(注意這⾥的區間都是開區間,也就是不包括邊界值)。
- next-key lock:record lock+gap lock,所以next-key lock也就半開半閉區間,且是下界開,上界閉。
加鎖規則特性
加鎖規則有⼀些特性,其中我們需要關注的有:
- 加鎖的基本單位是(next-key lock),他是前開後閉原則
- 查詢過程中訪問的物件會增加鎖
- 間隙鎖僅阻⽌其他事務插⼊間隙。在刪除資料的時候,會去加間隙鎖,但是多個事務是可以同時對⼀個間隙去加鎖的,⽽如果需要對該間隙進⾏插⼊,則需要等待鎖釋放。
解決方式
1、將事務隔離級別將為read commit.
間隙鎖只存在於可重複讀的隔離級別下,因為要防⽌幻讀。這個⽅法不現實,不可能為了這個問題 把整個線上資料庫隔離級別給改掉。
2、避免先刪除後插⼊的操作.
修改程式碼,避免先刪除後插⼊的操作。犧牲效能,在業務中,先根據唯⼀索引查出存在的記錄,然後對存在的記錄進⾏根據主鍵Id在迴圈中更新,對於不存在的記錄進⾏批量插⼊。