關鍵詞:MySQL Index Mergemysql
MySQL 的鎖機制相信你們在學習 MySQL 的時候都有簡單的瞭解過,那既然有鎖就一定繞不開死鎖這個問題。其實 MySQL 在大部分場景下是不會存在死鎖問題的(好比並髮量不高,SQL 寫得不至於太拉胯的狀況),可是在高併發的業務場景下,一不注意就會產生死鎖,而這個死鎖分析起來也比較麻煩。git
前段時間在公司實習的時候就遇到了一個比較奇怪的死鎖,以前一直沒來得及好好整理,最近有空復現了一下,算是積累一點經驗。github
簡單說一下業務背景,公司作的是電商直播,我負責的是主播端相關的業務。而這個死鎖就出如今主播後臺對商品信息進行更新的時候。sql
咱們的一個商品會有兩個關聯的 ID,經過其中任何一個 ID 都沒法肯定惟一一件商品(也就是說這個 ID 和商品是一對多的關係),只能同時查詢兩個 ID,才能肯定一件商品。因此在更新商品信息的時候,須要在 where 條件中同時指定兩個 ID,下面是死鎖 SQL 的結構(已脫敏):數據庫
UPDATE test_table SET `name`="zhangsan" WHERE class_id = 10 AND teacher_id = 8;
這個 SQL 很是簡單,根據兩個等值條件,對一個字段進行更新。併發
不知道你看到這個 SQL 會不會懵逼,按常理來講,應該是一個事務裏有多條 SQL 纔會有可能出現死鎖,這一條 SQL 怎麼可能出現死鎖呢?高併發
是的,我當時也有這樣的疑惑,甚至懷疑是否是報警系統瞎報(最後證實不是…),當時是真的摸不着頭腦。而且由於數據庫權限的緣由,想看死鎖日誌都看不到,又是臨近下班的時候,找 DBA 能麻煩死,因此就直接搜索引擎走起了……(關鍵詞:update 死鎖 單條 sql),最後查出來是因爲 MySQL 的索引合併優化致使的,即 Index Merge,下面會進行詳細講解並復現一下死鎖場景。性能
Index Merge 是 MySQL 在 5.0 的時候引入的一項優化功能,主要是用於優化一條 SQL 使用多個索引的狀況。學習
咱們來看剛剛的 SQL,假設 class_id
和 teacher_id
分別是兩個普通索引:測試
UPDATE test_table SET `name`="zhangsan" WHERE class_id = 10 AND teacher_id = 8;
若是沒有 Index Merge 優化的時候,MySQL 查詢數據的步驟以下:
class_id
的索引)在二級索引上查詢到對應數據的主鍵 IDteacher_id
,判斷其是否等於 8,知足條件則返回從這個過程當中,不難看出,MySQL 只使用到了一個索引,至於爲何不使用多個索引,簡單來講就是由於多個索引在多棵樹上,強行使用反而下降性能。
再來看看引入了 Index Merge 優化後,MySQL 查詢數據的步驟以下:
class_id
查詢到相應的主鍵,再根據主鍵回表查詢到對應的數據行(記爲結果集 A)teacher_id
查詢到相應的主鍵,再根據主鍵回表查詢到對應的數據行(記爲結果集 B)這裏能夠看出,有了 Index Merge 以後,MySQL 將一條 SQL 語句拆分紅了兩個查詢步驟,分別使用兩個索引,再用交集操做優化性能。
分析完了 Index Merge 的步驟,咱們再回過頭想一下爲何會出現死鎖呢?
還記得上面說的 Index Merge 將一條 SQL 查詢拆分紅了兩個步驟嗎,問題就出如今這裏。咱們知道 UPDATE
語句是會加上一個行級排他鎖的,在分析加鎖步驟以前,咱們假設有以下一個數據表:
上表數據知足咱們文章開頭說的特色,根據 class_id
和 teacher_id
單個字段均沒法惟一肯定一條數據,只能聯合兩個字段,才能肯定一條數據,而且設定 class_id
和 teacher_id
分別爲兩個普通索引。
假設有以下兩條 SQL 語句併發執行,它們的參數徹底不一樣,直覺告訴咱們應該不會出現死鎖,但直覺每每是錯誤的:
// 線程 A 執行 UPDATE test_table SET `name`="zhangsan" WHERE class_id = 2 AND teacher_id = 1; // 線程 B 執行 UPDATE test_table SET `name`="zhangsan" WHERE class_id = 1 AND teacher_id = 2;
那麼在 Index Merge 的優化下,併發執行如上 SQL 的時候,MySQL 的加鎖步驟以下:
最終,兩個事務互相等待,造成死鎖
由於這個死鎖本質上仍是因爲 Index Merge 這個優化致使的,因此要解決這個場景的死鎖問題,本質上只要讓 MySQL 不走 Index Merge 優化便可。
方案一
手動將一條 SQL 拆分紅多條 SQL,在邏輯層作交集操做,阻止 MySQL 的憨憨優化行爲,好比這裏咱們能夠先根據 class_id
查詢到相應主鍵,再根據 teacher_id
查詢相應主鍵,最後根據交集後的主鍵查詢數據。
方案二
創建聯合索引,好比這裏能夠將 class_id
和 teacher_id
創建一個聯合索引,MySQL 就不會走 Index Merge 了
方案三
強制走單個索引,在表名後添加 for index(class_id)
能夠指定該語句僅走 class_id 索引
方案四
關閉 Index Merge 優化:
SET [GLOBAL|SESSION] optimizer_switch='index_merge=off';
UPDATE /*+ NO_INDEX_MERGE(test_table) */ test_table SET
name="zhangsan" WHERE class_id = 10 AND teacher_id = 8;
爲了方便測試,這裏提供一個 SQL 腳本,將其用 Navicat 導入後便可獲得須要的測試數據:
下載地址:https://cdn.juzibiji.top/file/index_merge_student.sql
導入以後,咱們會獲得以下格式的 10000 條測試數據:
因爲篇幅限制,這裏僅給出代碼 Gist 連接:https://gist.github.com/juzi214032/17c0f7a51bd8d1c0ab39fa203f930c60
上述代碼主要是開啓 100 個線程執行咱們的數據修改 SQL 語句,來模擬線上併發狀況,在運行幾秒鐘後,咱們會獲得下面這樣一個報錯:
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
這表明已經產生了死鎖異常
上面咱們用代碼已經構造出了一個死鎖,接下來咱們進入 MySQL 看看死鎖日誌,在 MySQL 中執行以下命令便可查看死鎖日誌:
SHOW ENGINE INNODB STATUS;
在日誌中,咱們找到 LATEST DETECTED DEADLOCK
這一行,這裏開始即是咱們上次產生的死鎖,接下來咱們開始分析。
經過第 29 行能夠看到,事務 1 執行的 SQL 的條件是 class_id = 6
和 teacher_id = 16
,它目前持有了一個行鎖,第 34~39 行是該行數據,34 行是主鍵的十六進制表示,咱們轉換爲 10 進制即爲 1616。一樣的,看 45 行,其等待拿鎖的是主鍵 id 1517 的數據。
接下來用一樣的方法分析事務 2,可知事務 2 持有了 3 把鎖,分別是主鍵 id 爲131七、141七、1517 的數據行,等待的是 1616 。
看到這裏咱們就已經發現了,事務 1 持有 1616 等待 1517,事務 2 持有1517 等待 1616,因此造成了一個死鎖。此時 MySQL 的處理方法是回滾持有鎖最少的事務,而且 JDBC 會拋出咱們前面的 MySQLTransactionRollbackException 回滾異常。
這個死鎖在排查的時候其實很是很差排查,若是你不知道 MySQL 的 Index Merge,那麼在排查的時候實際上是毫無頭緒的,由於呈如今你面前的就只有一條很是簡單的 SQL,就算看死鎖日誌,也是同樣的不明因此。
因此處理這類問題,更多的仍是考驗你的知識儲備量和經驗,只要遇到過一次,後面在寫 SQL 的時候多加註意就行了!