1. 程式人生 > 實用技巧 >P2764 最小路徑覆蓋問題 網路流

P2764 最小路徑覆蓋問題 網路流

前幾天線上收到一條告警郵件,生產環境MySQL操作發生了死鎖,郵件告警的提煉出來的SQL大致如下。

update pe_order_product_info_test
        set  end_time = '2021-04-30 23:59:59'
        where order_no = '111111111'
        and product_id = 123456
        and status in (1,2);
update pe_order_product_info_test
        set  end_time = '2021-04-30 23:59:59'
        where order_no = '222222222'
        and product_id = 123456
        and status in (1,2);      

是一條Update語句,定位了它的呼叫情況,發現Update的呼叫方只有一處,並且在Cat中看到一個小時的呼叫次數只有700多次,這個呼叫量基本與併發Update引起死鎖無關了。

當時猜測了幾種情況,這裡Update進行操作時有其他業務方呼叫Select相關的介面,但是排查了那個時間點發生死鎖應用的呼叫鏈,發現好像並沒有其他會影響到Update的呼叫。

為了更進一步瞭解當時的情況,就聯絡了DBA老師,要了當時死鎖發生時的日誌,準備拿到日誌之後大幹一場,好好分析一下問題,結果...

DBA老師看了死鎖日誌直接點出了問題要害——index_merge索引合併。

1. 什麼是索引合併

這是MySQL在5.1引入的優化技術,再此之前,一個表僅僅只能使用一個索引,但索引合併的引入,可以對同一張表使用多個索引分別進行條件掃描。

如果要拿索引合併index_merge與只使用一個索引做比較,那麼拿上面那個update語句來做演示。

update pe_order_product_info_test
        set end_time = '2021-04-30 23:59:59'
        where order_no = '111111111'
        and product_id = 123456
        and status in (1,2);

只是用一個索引時,MySQL會選擇一個最優的索引來使用,比如使用index_order_no,拿它來找出所有order_no為111111111的索引記錄,從該索引上找到它的PRIMARY

索引的id,然後回表找到對應的行資料,最後在記憶體中根據剩下的product_id和status條件來進行過濾。

但如果MySQL優化器覺得你如果只是用一個索引,拿出大量記錄,然後再在記憶體中使用product_id和status過濾(並且符合該條件的記錄值很少),這個第二步效率可能不高時,他就會使用索引合併進行優化。

如果使用索引合併去判斷where條件時,那麼它就會先通過index_order_no索引去找到PRIMARY索引的id,再通過index_product_id索引去找到PRIMARY索引的id,最後將兩個id集合求交集,再回表找到行資料。(索引合併使用索引的順序是不確定的)

2. 場景復現

在MySQL的Bug反饋文件中也有記錄一個Bug #77209的記錄,標註了索引合併引發死鎖的情況。但是我按照它給出的repeat並不能重現索引合併的場景,在它的例項中早了600萬隨機數,我猜測可能是MySQL調高了索引合併的條件,將資料量增加到了1000萬。

先來帶大家復現一下當時的情況。

環境:MySQL 5.6.24

  1. 建立一張測試表

    CREATE TABLE `a` (
      `ID` int  AUTO_INCREMENT PRIMARY KEY,
      `NAME` varchar(21),
      `STATUS` int,
      KEY `NAME` (`NAME`),
      KEY `STATUS` (`STATUS`)
    ) engine = innodb;
    
  2. 匯入資料,為了方便匯入一些隨機資料,需要先開啟一個相容性配置。

    set global show_compatibility_56=on;  
    

    開始匯入隨機資料。

    set @N=0;
    insert into a(ID,NAME,STATUS)
    select
    	@N:=@N+1,
    	@N%1600000, 
    	floor(rand()*4)
     from information_schema.global_variables a, information_schema.global_variables b, information_schema.global_variables c 
    LIMIT 10000000;
    
  3. 測試

    update a set status=5 where rand() < 0.005 limit 1;
    explain UPDATE a SET STATUS = 2 WHERE NAME =  '1000000' AND STATUS = 5;
    

3. 為什麼發生了死鎖

直接上一副圖,以及兩個update事務的加鎖流程。

可以看到在訂單與產品這個模型中,Update事務一和Update事物二在product_id索引和primary索引上都存在交叉重合,這就導致了死鎖的發生。

步數 事務一 事務二
1 鎖住index_order_no索引樹上order_no為2222的索引項
2 鎖住index_order_no索引樹上order_no為3333的索引項
3 回表鎖住 PRIMARY 索引中 id 為 11 的索引項
4 回表鎖住 PRIMARY 索引中 id 為 12 的索引項
5 鎖住index_product_id索引樹上product_id為2000的四個索引項
6 嘗試去鎖住index_product_id索引樹上product_id為2000的四個索引項,但是已經被事務一鎖住,等待事務一釋放index_product_id上的鎖
7 試圖回表鎖住 PRIMARY 索引中 id 為10,11,12,13的索引項,發現id為12的索引項在第4步已經被事務二鎖住,等待事務二釋放

這就是本次死鎖發生的原因所在了,解決方案有很多種,可以根據具體場景選擇。

  1. 刪除某一個索引,這當然不是一個好辦法
  2. 關閉index_merge優化
  3. 為查詢條件增加聯合索引,在本例中是product_id和order_no。

4. 最後

當然最後這些都是我個人的分析,DBA老師給的建議是直接上聯合索引,網上關於索引合併的資料實在太少了,除了官方文件簡單扯了扯,剩下的都是轉載來轉載去的部落格,內容都一模一樣,DBA老師也不寫部落格,所以我就只能按我上述這個思路理解了,如果網友有什麼問題歡迎指出~