表 preferences 僅有主鍵 numericId,無資料。兩個事務並發執行 UPDATE ... WHERE numericId = 不存在的值,隨後各自 INSERT 該值,觸發死鎖。
(prev, curr],InnoDB REPEATABLE READ 預設鎖模式。BEGIN; UPDATE preferences SET ... WHERE numericId = 1; → 未命中記錄,獲得 (-∞, 1] Next-Key Lock(Gap Lock 部分)。BEGIN; UPDATE preferences SET ... WHERE numericId = 2; → 獲得 (1, 2] Next-Key Lock。INSERT INTO preferences VALUES (1, ...); → 需在 (-∞, 1] 間隙插入,請求插入意向鎖,被 Session B 持有的 (1, 2] Gap Lock 封鎖(因 1 在該區間邊界),進入 LOCK WAIT。INSERT INTO preferences VALUES (2, ...); → 同理被 Session A 的 Gap Lock 封鎖 → 死鎖,InnoDB 檢測回滾 Session B。| 策略 | 原理 | 適用場景 |
|---|---|---|
| 改用 REPLACE INTO / INSERT ... ON DUPLICATE KEY UPDATE | 單語句原子化,避免「先查後寫」間隙 | 業務允許覆蓋寫入 |
| 降級隔離級別 READ COMMITTED | RC 下無 Gap Lock,僅 Record Lock | 可接受不可重複讀、無幻讀需求 |
| innodb_locks_unsafe_for_binlog=ON (已棄用) | 關閉 Gap Lock,僅用於測試 | 不建議生產 |
| 應用層樂觀鎖(version 欄位) | 避免長事務持有鎖 | 高併發更新熱點行 |
| 預先 INSERT 佔位行 | 消除間隙,變 Record Lock 競爭 | 業務可預先初始化 |
-- Session 1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
UPDATE preferences SET ... WHERE numericId = 1; -- 僅 Record Lock,無 Gap Lock
-- Session 2 同理,INSERT 不會被 Gap Lock 封鎖,僅主鍵衝突等待
SHOW ENGINE INNODB STATUS\G
-- 關注 LATEST DETECTED DEADLOCK 區塊,分析鎖等待鏈
SELECT * FROM performance_schema.data_locks WHERE ENGINE='INNODB';
SELECT * FROM sys.innodb_lock_waits;
InnoDB 間隙鎖是 REPEATABLE READ 防幻讀的代價。「不存在記錄的 UPDATE/DELETE 會持有 Gap Lock」 是死鎖高發根因。架構層面:優先用單語句 UPSERT 替代「查詢-決策-寫入」模式;必要時降級 RC 隔離級別。應用層:縮短事務、避免長事務、監控 Innodb_deadlocks 計數器告警。