轉自:咖啡拿鐵 作者:咖啡拿鐵
1.背景
最近有一些活動,於是會對系統做一些平時量比較小的路徑做一些打壓,這不打壓還好,這一打壓就出現了奇怪的問題,居然有一段陳年老代碼出現了死鎖的問題,日誌如下:
看見了日誌之後,就踏上了死鎖的排查之路。當然如果你對鎖不是很熟悉的話你可以先看我的這兩篇文章看一下數據庫鎖的基礎知識: 爲什麼開發人員必須要了解數據庫鎖:和記一次神器的mysql死鎖排查
2.問題分析
數據庫代碼如下:
CREATE TABLE `order_extrainfo` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`orderId` int(11) NOT NULL,
`extraInfo` text NOT NULL,
`appProductId` int(11) NOT NULL DEFAULT '0',
`hostAppProductId` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `orderId` (`orderId`)
) ENGINE=InnoDB AUTO_INCREMENT=17835265 DEFAULT CHARSET=utf8mb4;
出現問題的sql語句如下:
INSERT INTO `order_extrainfo` (orderId, " +
"extraInfo, appProductId, hostAppProductId) VALUES (?,?, ?, ?) ON " +
"DUPLICATE KEY UPDATE extraInfo = ?, appProductId = ?, " +
"hostAppProductId = ?
之前沒有遇到過insert出死鎖的情況,所以當時覺得是on dpulicate key update導致的。爲了找到當時的死鎖現場,輸入:SHOW ENGINE INNODB STATUS;查看最近一次死鎖日誌:
事務1和事務2明顯都在等待gap鎖的釋放,應該是互相持有gap鎖,都在等待對方導致。
一般的死鎖日誌都是由兩個事務導致的,所以會給予一定的迷惑性,其實大部分的死鎖都是由兩個以上的事務導致的,這次其實也不例外,這其實是mysql的一個bug,https://bugs.mysql.com/bug.php?id=52020,
有興趣的可以看一下。
對着bug中的描述,在本地開始復現一下這個場景:
時間線 | session1 | session2 | session3
---|--- |---|---|---
1 | begin; | | insert into xx
2 | INSERT INTO order_extrainfo
(orderId, extraInfo, appProductId, hostAppProductId) VALUES (158360183,'', 0, 0) ON DUPLICATE KEY UPDATE extraInfo = '';|begin; |
3 |1 row in affected; | INSERT INTO order_extrainfo
(orderId, extraInfo, appProductId, hostAppProductId) VALUES (158360184,'', 0, 0) ON DUPLICATE KEY UPDATE extraInfo = '';| begin;
4 | | | INSERT INTO order_extrainfo
(orderId, extraInfo, appProductId, hostAppProductId) VALUES (158360184,'', 0, 0) ON DUPLICATE KEY UPDATE extraInfo = '';
5 | commit;|
6 | | 1 row in affected;|Deadlock found when trying to get lock; try restarting transaction
注意:session1,2,3 具體每個orderId 是依次遞增的
session3 出現了死鎖。
session1,2,3 的這個執行順序在我們的高併發的時候是很容易出現的,所以纔會大量出現死鎖報錯。
2.1 鎖分析
這裏我們來具體分析一下到底加了什麼鎖,我們知道insert插入操作的時候會加 X鎖和插入意向鎖,這裏我們看看 insert into on duplicate key加什麼鎖,
這個是在我本地電腦進行測試,首先打開:
set GLOBAL innodb_status_output_locks=ON;
set GLOBAL innodb_status_output=ON;
mysql的鎖統計,這個線上不推薦打開打開的話日誌會記錄得比較多。
首先執行第一個sql:
INSERT INTO order_extrainfo
(orderId, extraInfo, appProductId, hostAppProductId) VALUES (158360183,'', 0, 0) ON DUPLICATE KEY UPDATE extraInfo = '';
輸入show engine innodb status命令查看
加鎖情況如上圖所示,這裏要說明的是 insert intention 在這裏是隱式鎖,這裏加的鎖實際上就是x + GAP(負無窮到正無窮的gap鎖) + insert intention 三個鎖
這裏我們在執行執行第二個sql,
INSERT INTO order_extrainfo
(orderId, extraInfo, appProductId, hostAppProductId) VALUES (158360184,'', 0, 0) ON DUPLICATE KEY UPDATE extraInfo = '';
發現其插入意向鎖正在被gap鎖阻塞。
同樣的如果我們執行第三個sql,插入意向鎖也會被第一個事務gap鎖阻塞,如果第一個事務的gap鎖提交,他們首先又會先獲取gap鎖(這裏從鎖的信息判斷,被阻塞的時候是沒有gap鎖),其次再獲取插入意向鎖,就導致了session2,session3兩個形成循環鏈路,最終導致死鎖。
2.2 爲什麼會有gap鎖
gap鎖是RR隔離級別下用來解決幻讀的一個手段,一般出現在delete中,爲什麼會出現在這裏呢?在 https://bugs.mysql.com/bug.php?id=50413 這個bug中可以看見:
"Concurrent "INSERT …ON DUPLICATE KEY UPDATE" statements run on a table
with multiple unique indexes would sometimes cause events to be written to
the binary log incorrectly"
”
當我們併發的用INSERT …ON DUPLICATE KEY UPDATE的時候,如果我們有多個唯一索引,那麼有可能會導致binlog錯誤,也就是會導致主從複製不一致,具體的一些測試可以去鏈接中查看
3.如何解決
如果遇到這個問題怎麼辦呢?我們有下面的一些方法來解決這個問題:
使用mysql5.6版本,可以看見這個是在5.7中引入的,5.6中不會出現這個情況
使用RC級別,RC隔離級別下不會有gap鎖
-- 不要使用 insert on duplicate key update,使用普通的insert。我們最後使用的就是這個方法,因爲ON DUPLICATE KEY UPDATE 這個在代碼中的確是沒有必要在數據庫表中只建立主鍵,不建立其他唯一索引。
先insert 再捕獲異常,然後進行更新
使用insert ignore,然後判斷update rows 是否是1,然後再決定是否更新。
如果大家覺得這篇文章對你有幫助,你的關注和轉發是對我最大的支持,O(∩_∩)O:
相關推薦
掃碼領取學習資料
後臺回覆 學習資料 即可領取
如有收穫,點個在看,誠摯感謝