案例一:
有一張表A,先更新,如果影響行數爲0,則執行INSERT插入數據。很常見的場景,在生產上也跑了很久,沒有出現什麼問題。但是有一次在測試環境做壓測時居然出現了死鎖,Deadlock found when trying to get lock; try restarting transaction
因爲對mysql鎖不熟悉,爲什麼insert也會死鎖,不是一般在update的時候會死鎖嗎? 很好奇,於是開始尋找原因…
mysql鎖是跟數據庫設置的隔離級別有關係的,不同的隔離級別,鎖也各不相同,只要是爲了解決類似髒讀、幻讀、可重複讀的問題。
select @@tx_isolation; – 查看隔離級別
隔離級別是RR(可重複讀),我們知道,在此隔離級別下,爲了解決幻讀的問題,會有存在間隙鎖和Next-key lock(行鎖 + 間隙鎖),即在update、delete語句後會產生間隙鎖和Next-key lock,如果在併發下,存在兩個事務都事前執行了UPDATE語句(各自持有了gap lock),當INSERT時,要先在插入間隙上獲取插入意向鎖,由於插入數據的間隙存在衝突,所以會互相等待獲取插入意向鎖,即相互競爭,最終會導致一方死鎖。
這也解釋了爲什麼在測試環境出現死鎖。
解決:
- 不採用事務,即無事務方式執行,但是如果出現異常則會出現數據不一致的情況。
- 調整事務隔離級別爲read commit,RC級別不會產生gap lock
由於我們都知道事務的重要性,所以選擇第二種方式,將隔離級別改爲RC。我們在生產環境一般也就這個級別,所以也解釋了爲什麼在生產沒有出現這個問題。
事務隔離級別調整可以通過spring的聲明式事務方式來實現,通過註解@Transactional
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
public void calculationEntry(List<CampaignFeedback> campaignFeedbackList, String groovyScript) {
//todo
}
再次壓測,死鎖問題不再出現。
案例二:
由於業務需要,經常對某張表做更新或者寫入,又不想每次都先查詢是否存在再插入,所以使用了mysql的replace into來實現。
replace into 語法是如果表中存在主鍵或者唯一索引,它會按這個維度去判斷是否存在,如果存在則會先delete在insert,即它會分開兩步來操作,不保證原子性。
開始使用後覺得還是很方便的,畢竟不用自己去多查詢一次,mysql自動幫我們完成了。 但是到了壓測環節,死鎖竟然又出現了。。。。。。
因爲有了前面的經驗,所以這都不是問題,於是按之前的方式改了。在測試環境跑了幾次都沒問題,以爲都是一樣的問題導致,所以就上線了。
上線後,當我去驗證這個功能的時候,尼瑪。。。死鎖問題再一次出現在我的眼前。
我很慌,爲什麼這次不行,難道我代碼被覆蓋了? 翻看git的提交記錄並沒有。。
於是我們去諮詢了DBA,他說replace into 這個不建議使用,因爲在大併發環境下,肯定會出現死鎖。建議我們使用 insert into … values … on duplicate key update … 來替代
於是按DBA建議改了,再次上線後,不管怎麼併發都不會出現死鎖。
原因分析,如果使用replace into mysql會默認開啓Gap lock和Next-key lock,所以即使加了事務配置也不起作用。
最後附上 insert into … on duplicate key update 介紹:
語法:
INSERT INTO tablename (field1,field2, field3, …) VALUES(value1, value2, value3, …) ON DUPLICATE KEY UPDATE field1=value1,field2=value2, field3=value3, …;
也可以使用以下方式(這種支持批量插入)
INSERT INTO TABLE (a,b,c) VALUES
(1,2,3),
(2,5,7),
(3,3,6),
(4,8,2)
ON DUPLICATE KEY UPDATE a=VALUES(a), b=VALUES(b),c=VALUES(c);