在高併發業務中,多台機器需從同一張表獲取數據並避免重複處理。採用 SELECT ... FOR UPDATE 加排他鎖(X 鎖)是常見方案,但若與其他 UPDATE 語句交互不當,極易引發死鎖。本文通過實戰案例深入剖析 InnoDB 鎖機制,揭示死鎖根因並給出解決思路。
SELECT ... FOR UPDATE LIMIT 0,30 鎖定前 30 條,其他機器等待鎖釋放FOR UPDATE SKIP LOCKED 可跳過已鎖行,MySQL 5.7+ 才引入此特性表結構:id 為主鍵,IsSuccess、GetTime 為非主鍵索引。初始數據 id=400000,IsSuccess=0、GetTime=0。
Session 1:
SET autocommit=0; BEGIN; SELECT * FROM table1 WHERE getTime < 1 AND IsSuccess=0 ORDER BY id ASC LIMIT 0,30 FOR UPDATE; -- 不提交,持有鎖
Session 2:
UPDATE table1 SET IsSuccess=0 WHERE id=400000; -- 阻塞等待
原因: Session 1 的範圍查詢鎖定了 IsSuccess=0 的二級索引區間,Session 2 更新同一索引值導致衝突。若改為 IsSuccess=2 則不阻塞,因索引值不同。
先將 id=400000 的 GetTime 改為 1,IsSuccess=0。
Session 1:
BEGIN; UPDATE table1 SET IsSuccess=1 WHERE id=400000; -- 不提交
Session 2:
SELECT * FROM table1 WHERE getTime < 1 AND IsSuccess=0 ORDER BY id ASC LIMIT 0,30 FOR UPDATE; -- 阻塞!
深度解析:
IsSuccess(值從 0→1)IsSuccess=0,需讀取該二級索引id=400000 的 IsSuccess 值不確定(可能回滾),Session 2 必須等待GetTime=1 已不滿足 < 1,但 InnoDB 按索引加鎖,仍會掃描並嘗試鎖定該記錄真實業務中發現:SELECT ... FOR UPDATE 與另一條 UPDATE 導致死鎖。
SELECT ... FOR UPDATE 先鎖主鍵索引 → 再鎖二級索引UPDATE 先鎖二級索引 → 再鎖主鍵索引LIMIT 20,30 若第 1 行被鎖,同樣會等待。GetTime < 1 會鎖定從第一行開始的所有符合條件範圍。GetTime < 1 改為 GetTime = 0(等值查詢),移除 IsSuccess 條件,避免範圍鎖擴大。SELECT ... FOR UPDATE SKIP LOCKED 可直接跳過已鎖行。-- 查看正在執行的線程 SHOW PROCESSLIST; -- 終止指定線程(殺死查詢,保留連線) KILL QUERY thread_id; -- 或終止整個連線 KILL CONNECTION thread_id;
需具備 PROCESS 權限查看所有線程,SUPER 權限可殺死任意線程。
InnoDB 的行鎖本質是鎖索引,理解索cesi加鎖順序、範圍查詢鎖定範圍、條件列檢查機制,是避免死鎖的關鍵。實際開發中應優化 SQL 條件、統一訪問順序、控制事務粒度,並善用 SHOW ENGINE INNODB STATUS 分析死鎖日誌。