空表執行 UPDATE ... WHERE pk = 不存在值,預期無鎖,實際持有 X GAP LOCK(間隙鎖),導致並發插入死鎖。
CREATE TABLE `preferences` (
`numericId` INT UNSIGNED NOT NULL,
`receiveNotifications` TINYINT(1) DEFAULT NULL,
PRIMARY KEY (`numericId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- 表為空
SELECT COUNT(*) FROM preferences; -- 0
-- Session 1 BEGIN; UPDATE preferences SET receiveNotifications=1 WHERE numericId = 1; -- 0 rows affected -- 持有 GAP X LOCK on (supremum pseudo-record) -- Session 2 BEGIN; UPDATE preferences SET receiveNotifications=1 WHERE numericId = 2; -- 0 rows affected -- 持有 GAP X LOCK on (supremum pseudo-record) -- Session 1 INSERT INTO preferences VALUES (1, 1); -- LOCK WAIT(等待 Session 2 的 GAP 鎖) -- Session 2 INSERT INTO preferences VALUES (2, 1); -- ERROR 1213: Deadlock found
---TRANSACTION 4A18101, ACTIVE 12 sec 2 lock struct(s), heap size 376, 1 row lock(s) RECORD LOCKS space id 31766 page no 3 n bits 72 index `PRIMARY` of table `test`.`preferences` trx id 4A18101 lock_mode X -- GAP X 鎖
InnoDB 使用 Next-Key Lock(記錄鎖 + 間隙鎖)防止 Phantom Read(幻讀)。
REPEATABLE READ 隔離級別(預設)要求:事務期間重複查詢結果集不變WHERE pk = 1 掃描主鍵索引,發現無記錄,為防止其他事務插入 pk=1,必須鎖住「不存在的位置」(-∞, 1)、(1, 2)、(2, +∞) 等區間| 方案 | 原理 | 優點 | 缺點 | 推薦度 |
|---|---|---|---|---|
改用 READ COMMITTED 隔離級別 | RC 不加間隙鎖,僅鎖存在記錄 | 徹底解決、無代碼變更 | 可能出現幻讀、需評估業務接受度 | ★★★★★ |
使用 REPLACE INTO / INSERT ... ON DUPLICATE KEY UPDATE | 單條語句原子化,避免先 UPDATE 再 INSERT | 業務邏輯簡化、無鎖升級 | 語意不同(REPLACE 會刪除舊行觸發 FK/Trigger) | ★★★★☆ |
| 應用層檢查存在性再決定操作 | 先 SELECT FOR SHARE/UPDATE,再 INSERT/UPDATE | 邏輯清晰 | 雙次查詢、仍有競態窗口 | ★★★☆☆ |
設定 innodb_locks_unsafe_for_binlog=1 | 禁用間隙鎖(已廢棄/移除) | - | 不安全、不可用 | ❌ |