多機器併發從同一表獲取任務時,使用 SELECT ... FOR UPDATE LIMIT n 鎖定行防止重複消費,卻意外觸發死鎖。本文剖析 InnoDB 鎖機制、死鎖成因、排查方法與優化方案。
SELECT ... FOR UPDATE LIMIT 30 鎖定前 30 行Deadlock found when trying to get lock; try restarting transactionInnoDB 行鎖實際鎖定的是**索引記錄**:
WHERE 條件命中**主鍵索引** → 鎖主鍵索引記錄task:主鍵 id,二級索引 statusSELECT * FROM task WHERE status=0 ORDER BY id LIMIT 30 FOR UPDATE(處理 31-60)UPDATE task SET status=1 WHERE id IN (1,2,3)(更新 1-10)SELECT ... FOR UPDATE:
status 索引定位 status=0 的記錄id 排序UPDATE:
id 直接定位核心結論:**不同 SQL 語句訪問索引順序不一致,是死鎖的根本原因。** 即使操作不同行,鎖順序反轉也會死鎖。
-- 1. 查看最近死鎖信息
SHOW ENGINE INNODB STATUS\G
-- 關鍵輸出:LATEST DETECTED DEADLOCK
-- 列出:持有鎖、等待鎖、SQL文本、事務隔離級別
-- 2. 開啟死鎖記錄(MySQL 5.7+)
SET GLOBAL innodb_deadlock_detect = ON; -- 默認開啟
SET GLOBAL innodb_print_all_deadlocks = ON; -- 錯誤日志記錄所有死鎖
| 方案 | 原理 | 優點 | 缺點 |
|---|---|---|---|
| 統一索引訪問順序 | 強制所有 SQL 按主鍵順序訪問:SELECT * FROM task WHERE id IN (SELECT id FROM task WHERE status=0 ORDER BY id LIMIT 30) FOR UPDATE | 消除鎖順序反轉 | 子查詢可能產生臨時表,需測試性能 |
| SKIP LOCKED (MySQL 8.0+) | SELECT ... FOR UPDATE SKIP LOCKED LIMIT 30 跳過已鎖行,非阻塞獲取 | 無等待、無死鎖、吞吐極高 | 需 8.0+,舊版本不支持 |
| NOWAIT (MySQL 8.0+) | SELECT ... FOR UPDATE NOWAIT 遇鎖立即報錯,應用層重試 | 應用層控制重試邏輯 | 需處理錯誤重試,增加延遲 |
| 減少鎖範圍 | 僅鎖主鍵:SELECT id FROM task WHERE status=0 ORDER BY id LIMIT 30 FOR UPDATE 回表由應用層做 | 避免二級索引鎖 | 雙次查詢,應用層複雜 |
| 應用層分佈式鎖 | Redis/ZooKeeper 分配任務段,DB 僅做狀態更新 | DB 無鎖壓力,擴展性強 | 引入外部依賴,架構複雜 |
| 樂觀鎖替代 | 表加 version 列,UPDATE ... SET version=version+1 WHERE id=? AND version=? | 無鎖、高併發 | 衝突重試率高時效率低 |
FOR UPDATE SKIP LOCKED,併發任務隊列的終極解法FOR UPDATE 後立即處理並提交,避免長事務持有鎖innodb_print_all_deadlocks=ON + 日志分析,死鎖率 > 0.1% 需優化SELECT ... FOR UPDATE 看似簡單,實則涉及 InnoDB 複雜的索引鎖協議。**死鎖本質是鎖獲取順序的循環等待**。MySQL 8.0 的 SKIP LOCKED 從根源解決了併發任務獲取的死鎖難題,強烈建議升級。舊版本請務必統一索引訪問順序、縮小事務、引入應用層協調機制。