記一次Index-Merge造成的死鎖

起因

前一段時間一個沒有多少量的項目突然線上出錯報警,第一時間查到異常日誌

報錯信息比較明顯,數據庫產生死鎖。

分析

分析代碼之前讓我們來複習一下什麼是死鎖以及產生死鎖的原因是什麼

死鎖產生原因是什麼❓

當兩個及以上的事務,都在等待對方釋放已經持有的鎖或因爲加鎖順序不一致造成循環等待鎖資源。

舉個我們最常見的例子,A 事務持有X ,申請Y,B 事務持有Y鎖,申請X鎖。A和B 事務持有鎖並且申請對方持有的鎖,這樣就會造成死鎖。

翻譯成代碼:

// 在隔離級別RR,ID爲主鍵索引的情況下。 (畫外音:不談隔離級別與索引情況下分析加鎖都是耍流氓)                                

session1:update name = “a” where id =1;update name = “b” where id =4; session2:   update name = “a” where id = 4;update name = “b” where id = 1;

在併發的情況下,假設請求順序是這樣的1. session1先拿id=1的行鎖2. session2拿id=4的行鎖3. session1請求id=4的行鎖(等待session2釋放)4. session2請求id=1的行鎖(等待session1釋放)5. 循環等待,造成死鎖

瞭解了死鎖產生的基本原因之後,讓我們去看下源碼,看是不是有類似這樣的代碼邏輯。

但是奇怪的是,我們翻了源碼(這裏不把源碼放出來了),但是源碼並沒有類似這樣的邏輯,更神奇的是代碼里根本就沒有@Transaction註解,也就意味着沒有應用到事務,也就是說單表語句造成了死鎖❓

現在看來問題比較詭異,單條語句造成了死鎖。接下來我們去跟DBA要一下死鎖日誌

根據死鎖日誌,發現確實僅僅是因爲 

update g_growth_free_activity_product    SET buy_count = 1079    where product_id = 79550 and activity_id = 2062 and deleted = 0

這條語句產生了死鎖。

轉機

之後就是各種google,百度的時候了,終於我們發現了一些和我們比較像的案例,https://blog.csdn.net/zheng0518/article/details/54695605 ,鏈接裏的例子和我們的現象比較接近,文章裏更是貼出了MySQL官方bug的地址https://bugs.mysql.com/bug.php?id=77209

上面的圖就bug中描述的內容,大意是update時使用index merge增加了死鎖風險。

我們需要先去看看【index-merge】是什麼。
我們翻一下官方文檔:https://dev.mysql.com/doc/refman/8.0/en/index-merge-optimization.html,文檔裏有多種情況的介紹,不贅述。
翻譯一下大概就是對單個表的多個索引分別進行掃描並將結果交併集處理。

那我們再來看業務SQL,針對
update g_growth_free_activity_product    SET buy_count = 1079    where product_id = 79550 and activity_id = 2062 and deleted = 0
如果有index merge,意味着【product_id】和【activity_id】是索引列。(deleted字段應該沒人加索引吧)
我們去看下錶中的索引結構:

【product_id】和【activity_id】確實都是普通二級索引。
雖然都是單列索引,但是我們還不能確定優化器在執行SQL的時候一定會選擇【index merge】,還需要查看下執行計劃。
爲了保證我們的數據和線上一致,我們把線上數據拉了下來,並創建了一個test表,表結構相同,索引結構相同,把數據導進去。並查詢到發生死鎖時的請求日誌

我們通過執行計劃能看到,update時確實使用了 【index-merge】進行優化,extra列顯示的是使用了【交集】類型。

到這時候,所有的條件就都能對的上了,但是是否真的是因爲這個原因發生死鎖我們還需要還原一下案發現場,嘗試在neibu環境進行復現。
我們在上面已經建好的test表上做測試。

10個線程併發執行更新,查看結果。

確實是很容易就發生了死鎖。
到這裏我們問題就已經基本定位了:由於索引設置不合理的緣故,where條件兩個單列普通二級索引在查詢的時候MySQL進行了index-merge優化,引發死鎖。

分析

找到原因之後,我們再來分下下使用index-merge爲什麼會發生死鎖。

我們先拿到死鎖的數據。
查詢【activity_id=2062】,對應MySQL主鍵記錄是在 【88-234】
查詢【product_id=79550】,對應MySQL主鍵記是【218,186】
查詢【product_id=79466】,對應MySQL主鍵記是【219,183】我們根據執行計劃與加鎖流程拆分下成如下幾個過程

再結合的死鎖日誌,我們分析下加鎖流程

session1等待【activity_id】的鎖,session2等待的是主鍵鎖,產生循環等待,發生死鎖。

解決方案

最後說一下解決方案,建議使用方案2,本身就是因爲索引使用不合理導致,優化之後再也沒有死鎖的問題了。

1、關閉【index merge】
2、建立聯合索引
3、優化代碼
4、強制走單列索引

最後提幾個關於MySQL建議閱讀的文檔:
官方文檔:https://dev.mysql.com/doc/
淘寶數據庫內核月報:http://mysql.taobao.org/monthly/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章