記一次Update語句死鎖


業務背景

訂單服務和消息盒子服務使用典型的生產者消費者模式,
訂單狀態變更, 產生消息, 發往SQS隊列, 
消息盒子服務通過訂閱SQS隊列消費消息,更新DB中訂單消息的狀態。

出現死鎖問題

消息盒子服務多個線程消費訂單狀態消息, update DB中對應的訂單記錄, 出現死鎖。 update語句如下: 

update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and msg_key = ‘SO146213662’ and target = ‘201307438’.

看起來是很普通的update語句, 不應該出現死鎖。 

msgbox_message 表結構如下 (精簡版)

CREATE TABLE msgbox_message ( id BIGINT ( 20 ) UNSIGNED AUTO_INCREMENT, gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP, record_status TINYINT ( 3 ), msg_key VARCHAR ( 64 ), target VARCHAR ( 32 ) COMMENT 'uid', target_type TINYINT ( 3 ), url VARCHAR ( 255 ), PRIMARY KEY (id), KEY target(target, target_type), KEY msg_key(msg_key), KEY gmt_create(gmt_create) ) ENGINE = INNODB 

死鎖日志分析

show engine innodb status的顯示結果如下(刪除了不必要的部分): 

LATEST DETECTED DEADLOCK
2019-09-26 15:20:13
(1) TRANSACTION: TRANSACTION 149683649, thread id 659442, query id 956107029 IP1 ops_write updating
update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and msg_key = ‘SO146213662’ and target = ‘201307438
WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 118 page no 1399014 n bits 640 index target_idx of table cf_msgbox.msgbox_message;
trx id 149683649 lock_mode X locks rec but not gap waiting Record lock,
heap no 539 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 ——(索引樹的葉子節點數據)
0: len 9; hex 323031333037343338; asc 201307438;;
1: len 1; hex 00; asc ;;
2: len 8; hex 0000000004a135cb; asc 5 ;; ——(4a135cb是 16 進制的主鍵 ID 值77673931)
3: len 4; hex 5d8af714; asc ] ;;
..
(2) TRANSACTION: TRANSACTION 149683648, thread id 660492, query id 956107024 IP2 ops_write updating
update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and msg_key = ‘SOxxxxxxx’ and target = ‘201307438
HOLDS THE LOCK(S):
RECORD LOCKS space id 118 page no 1399014 n bits 640 index target_idx of table cf_msgbox.msgbox_message;
trx id 149683648 lock_mode X locks rec but not gap Record lock,
heap no 539 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 ——(索引樹的葉子節點數據)
0: len 9; hex 323031333037343338; asc 201307438;;
1: len 1; hex 00; asc ;;
2: len 8; hex 0000000004a135cb; asc 5 ;; ——(4a135cb是 16 進制的主鍵 ID 值77673931)
3: len 4; hex 5d8af714; asc ] ;;
WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 118 page no 1936298 n bits 152 index PRIMARY of table cf_msgbox.msgbox_message;
trx id 149683648 lock_mode X locks rec but not gap waiting Record lock,
heap no 13 PHYSICAL RECORD: ——(索引樹的葉子節點數據)
n_fields 18; compact format; info bits 0
0: len 8; hex 0000000004a135cb; asc 5 ;;—— (4a135cb是 16 進制的主鍵 ID 值77673931)
1: len 4; hex 5d8af714; asc ] ;;
…….. 省略一些字段
WE ROLL BACK TRANSACTION (2)

我把部分關鍵的信息重點標注了出來,其中thread id是mysql的兩個內部線程, 兩個IP地址就是消息盒子服務機器的IP地址, 兩條update語句只有where條件中的msg_key值不同。 

  • 事務 1 在等待 target_idx 索引樹 (index target_idx) 中葉子節點 (heap no 539 PHYSICAL RECORD) 的 X 鎖(互斥鎖)—標記為 1 號鎖🔒;
  • 事務 2 已持有 1 號鎖🔒, 在等待 PRIMARY 索引樹 (index PRIMARY) 中葉子節點 (heap no 13 PHYSICAL RECORD) 的 X 鎖—我們標記為 2 號鎖🔒。

僅憑上面的日志, 好像沒有死鎖。死鎖產生的必要條件是互相等待對方持有的鎖。
唯一的解釋是, 事務1的日志沒有全部輸出(不知道為啥), 事務1 必然持有了 2號鎖🔒。 死鎖---> 事務1 拿了2號鎖🔒 並等待 1號鎖;事務2 拿了 1號鎖🔒 並等待 2號鎖🔒 

怎么回事

二級索引樹(target_idx)和主鍵索引樹(PRIMARY) 的鎖🔒 都指向了id為77673931行記錄。 msgbox_message表中 id=77673931的結果如下: 
id gmt_create record_status msg_key target target_type url
77673931 2019-09-25 05:11:48 0 SO146213662 201307438 0 /orders#/SO146213662
msg_key列的值是 SO146213662, target列的值是201307438 事務1 中的where條件 利用target列的找到了該葉子節點,並等待該葉子節點的X鎖(1號鎖🔒),這是合理的。 事務2 中的where條件 利用target列的找到了該葉子節點,拿到了該葉子節點的X鎖(1號鎖🔒),這也是合理的。 但是 事務1 中的where條件 利用msg_key列可以查到 該行記錄, 它拿到了該行葉子節點的鎖(2號鎖🔒)的合理的。 事務2 中的where條件 利用msg_key列是查不到 該行記錄的, 它去等待 該行葉子節點的鎖(2號鎖🔒) 是不合理的。 

結論

explain該update語句的結果如下: 
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 UPDATE msgbox_message p1910 index_merge target,msg_key,gmt_create target,msg_key 131,259 NULL 1 100.00 Using intersect(target,msg_key); Using where
type 列標記為了 index_merge(即索引合並方式), key和Extra列說明
使用了target 和msg_key這兩個列的索引。關於 索引合並, 請移步官檔。
explain的結果表明, innodb同時根據二級索引 target_idx和msg_key, 查找到二級索引樹葉子節點中的主鍵ID值后,再根據主鍵ID索引查找行記錄。 那么索引合並為什么就引起了死鎖呢? 

引起死鎖的過程

兩種可能

  • 查找到即加鎖。
    先根據 msg_key 索引查找,對 msg_key 二級索引節點加 X 鎖, 然后對相應的主鍵節點加 X 鎖; 再根據 target 索引查找,然后對相應的主鍵節點加 X 鎖;(msg_key 的索引選擇性總是高於 target 索引)。這種方式 對於 造成死鎖 邏輯上是成立的。
葉子節點 事務 1 事務 2 葉子節點
msg_key 的索引節點 1 號鎖 (主鍵對應行記錄 a) X 鎖 1 成功 . .
. . X 鎖 2 成功 msg_key 的索引節點 2 號鎖 ( 主鍵對應行記錄
行記錄 a 4 號鎖 X 鎖 4 成功 . .
. . X 鎖 5 成功 行記錄 b 5 號鎖
. . X 鎖 3 成功 target 的索引節點 3(對應行記錄 a) 3 號鎖
. . 等待 X 鎖 4 .
target 的索引節點 3(對應行記錄 a) 3 號鎖 等待 X 鎖 3 . .
  • 查找,合並,加鎖
    根據 msg_key 和 target 索引對查找到的二級索引節點和主鍵節點 合並, 依次加鎖。由於合並后的結果是無序的,兩個事務 交叉對結果進行加鎖造成死鎖。 邏輯上是成立的,也包含了 上 面的情況。

好心辦壞事

mysql 查詢優化器 對於上述 sql 語句,其實不應該使用 index merge, 因為 msg_key 索引的選擇性很高, 完全不需要再使用 target 列的索引。這也說明查詢優化器並不是總能給出最優的結果。多次 explain 的結果表明,mysql 查詢優化器 有時候給出使用 msg_key 索引及 index pushdown 方式 (最優的結果)。
另外,為什么要使用 索引合並 ? 大多數情況下,索引合並可以減少 數據頁的 IO 訪問次數。查詢的時候可以直接根據索引合並后的結果集再去做 where 條件的過濾。但是不巧的是,update 的時候,根據索引合並的無序結果,加 X 鎖,導致了並發時死鎖(個人感覺這種問題屬於 bug)。

解決辦法

  • 一個是 sql 語句中 強制使用 單個索引列 (最優解)
    msg_key 列索引的選擇性高於 target 列, 那么 sql 指定索引
update msgbox_message set record_status = -1 **force index(msg_key)** where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and msg_key = 'SO146213662' and target = '201307438'. 
  • 另外一個, 業務代碼中先 select 查出主鍵 ID 值, 再根據主鍵 ID 值去 update(處理速度肯定會慢一些)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM