起因
前一段時(shí)間一個(gè)沒(méi)有多少量的項(xiàng)目突然線上出錯(cuò)報(bào)警哗咆,第一時(shí)間查到異常日志
報(bào)錯(cuò)信息比較明顯,數(shù)據(jù)庫(kù)產(chǎn)生死鎖尺锚。
分析
分析代碼之前讓我們來(lái)復(fù)習(xí)一下什么是死鎖以及產(chǎn)生死鎖的原因是什么
死鎖產(chǎn)生原因是什么?
當(dāng)兩個(gè)及以上的事務(wù)吼蚁,都在等待對(duì)方釋放已經(jīng)持有的鎖或因?yàn)榧渔i順序不一致造成循環(huán)等待鎖資源。
舉個(gè)我們最常見(jiàn)的例子辙谜,A 事務(wù)持有X 俺榆,申請(qǐng)Y,B 事務(wù)持有Y鎖装哆,申請(qǐng)X鎖罐脊。A和B 事務(wù)持有鎖并且申請(qǐng)對(duì)方持有的鎖定嗓,這樣就會(huì)造成死鎖。
翻譯成代碼:
// 在隔離級(jí)別RR萍桌,ID為主鍵索引的情況下宵溅。 (畫(huà)外音:不談隔離級(jí)別與索引情況下分析加鎖都是耍流氓) ? ? ? ? ? ? ? ? ? ? ? ?????????
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;
在并發(fā)的情況下藕施,假設(shè)請(qǐng)求順序是這樣的1. session1先拿id=1的行鎖2. session2拿id=4的行鎖3. session1請(qǐng)求id=4的行鎖(等待session2釋放)4. session2請(qǐng)求id=1的行鎖(等待session1釋放)5. 循環(huán)等待寇损,造成死鎖
了解了死鎖產(chǎn)生的基本原因之后,讓我們?nèi)タ聪略创a裳食,看是不是有類似這樣的代碼邏輯矛市。
但是奇怪的是,我們翻了源碼(這里不把源碼放出來(lái)了)诲祸,但是源碼并沒(méi)有類似這樣的邏輯浊吏,更神奇的是代碼里根本就沒(méi)有@Transaction注解,也就意味著沒(méi)有應(yīng)用到事務(wù)救氯,也就是說(shuō)單表語(yǔ)句造成了死鎖?
現(xiàn)在看來(lái)問(wèn)題比較詭異卿捎,單條語(yǔ)句造成了死鎖。接下來(lái)我們?nèi)ジ鶧BA要一下死鎖日志
根據(jù)死鎖日志径密,發(fā)現(xiàn)確實(shí)僅僅是因?yàn)?
update g_growth_free_activity_product? ? SET buy_count = 1079? ? where product_id = 79550 and activity_id = 2062 and deleted = 0
這條語(yǔ)句產(chǎn)生了死鎖午阵。
轉(zhuǎn)機(jī)
之后就是各種google,百度的時(shí)候了享扔,終于我們發(fā)現(xiàn)了一些和我們比較像的案例底桂,https://blog.csdn.net/zheng0518/article/details/54695605?,鏈接里的例子和我們的現(xiàn)象比較接近惧眠,文章里更是貼出了MySQL官方bug的地址https://bugs.mysql.com/bug.php?id=77209籽懦。
上面的圖就bug中描述的內(nèi)容,大意是update時(shí)使用index merge增加了死鎖風(fēng)險(xiǎn)氛魁。
我們需要先去看看【index-merge】是什么暮顺。
我們翻一下官方文檔:https://dev.mysql.com/doc/refman/8.0/en/index-merge-optimization.html,文檔里有多種情況的介紹秀存,不贅述捶码。
翻譯一下大概就是對(duì)單個(gè)表的多個(gè)索引分別進(jìn)行掃描并將結(jié)果交并集處理。
那我們?cè)賮?lái)看業(yè)務(wù)SQL或链,針對(duì)
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字段應(yīng)該沒(méi)人加索引吧)
我們?nèi)タ聪卤碇械乃饕Y(jié)構(gòu):
【product_id】和【activity_id】確實(shí)都是普通二級(jí)索引澳盐。
雖然都是單列索引祈纯,但是我們還不能確定優(yōu)化器在執(zhí)行SQL的時(shí)候一定會(huì)選擇【index merge】令宿,還需要查看下執(zhí)行計(jì)劃。
為了保證我們的數(shù)據(jù)和線上一致腕窥,我們把線上數(shù)據(jù)拉了下來(lái)粒没,并創(chuàng)建了一個(gè)test表,表結(jié)構(gòu)相同簇爆,索引結(jié)構(gòu)相同革娄,把數(shù)據(jù)導(dǎo)進(jìn)去。并查詢到發(fā)生死鎖時(shí)的請(qǐng)求日志
我們通過(guò)執(zhí)行計(jì)劃能看到冕碟,update時(shí)確實(shí)使用了 【index-merge】進(jìn)行優(yōu)化拦惋,extra列顯示的是使用了【交集】類型。
到這時(shí)候安寺,所有的條件就都能對(duì)的上了厕妖,但是是否真的是因?yàn)檫@個(gè)原因發(fā)生死鎖我們還需要還原一下案發(fā)現(xiàn)場(chǎng),嘗試在neibu環(huán)境進(jìn)行復(fù)現(xiàn)挑庶。
我們?cè)谏厦嬉呀?jīng)建好的test表上做測(cè)試言秸。
10個(gè)線程并發(fā)執(zhí)行更新,查看結(jié)果迎捺。
確實(shí)是很容易就發(fā)生了死鎖举畸。
到這里我們問(wèn)題就已經(jīng)基本定位了:由于索引設(shè)置不合理的緣故,where條件兩個(gè)單列普通二級(jí)索引在查詢的時(shí)候MySQL進(jìn)行了index-merge優(yōu)化凳枝,引發(fā)死鎖抄沮。
分析
找到原因之后,我們?cè)賮?lái)分下下使用index-merge為什么會(huì)發(fā)生死鎖岖瑰。
我們先拿到死鎖的數(shù)據(jù)叛买。
查詢【activity_id=2062】,對(duì)應(yīng)MySQL主鍵記錄是在 【88-234】
查詢【product_id=79550】蹋订,對(duì)應(yīng)MySQL主鍵記是【218率挣,186】
查詢【product_id=79466】,對(duì)應(yīng)MySQL主鍵記是【219露戒,183】我們根據(jù)執(zhí)行計(jì)劃與加鎖流程拆分下成如下幾個(gè)過(guò)程
再結(jié)合的死鎖日志椒功,我們分析下加鎖流程
session1等待【activity_id】的鎖,session2等待的是主鍵鎖智什,產(chǎn)生循環(huán)等待动漾,發(fā)生死鎖。
解決方案
最后說(shuō)一下解決方案撩鹿,建議使用方案2谦炬,本身就是因?yàn)樗饕褂貌缓侠韺?dǎo)致,優(yōu)化之后再也沒(méi)有死鎖的問(wèn)題了节沦。
1键思、關(guān)閉【index merge】
2、建立聯(lián)合索引
3甫贯、優(yōu)化代碼
4吼鳞、強(qiáng)制走單列索引
最后提幾個(gè)關(guān)于MySQL建議閱讀的文檔:
官方文檔:https://dev.mysql.com/doc/
淘寶數(shù)據(jù)庫(kù)內(nèi)核月報(bào):http://mysql.taobao.org/monthly/