InnoDB 行鎖實為「索引鎖」,SELECT ... FOR UPDATE 加排他鎖(X 鎖)時,鎖定的是符合條件的索引記錄。若不理解鎖升級、間隙鎖、Next-Key Lock 機制,極易在高併發場景觸發死鎖。本文透過實戰案例,深度剖析 FOR UPDATE 鎖定規則、死鎖成因、排查工具與解決方案。
測試表:id PK, getTime INT, IsSuccess INT(有索引),資料 id=400000, getTime=1, IsSuccess=0。
-- Session 1 SET autocommit=0; BEGIN; SELECT * FROM t WHERE getTime < 1 AND IsSuccess = 0 ORDER BY id LIMIT 30 FOR UPDATE; -- 不 COMMIT -- Session 2 UPDATE t SET IsSuccess = 0 WHERE id = 400000; -- 阻塞!
分析: Session 1 鎖定 IsSuccess=0 的輔助索引記錄(含 id=400000),Session 2 更新同值需等待 X 鎖釋放。
-- Session 1 同前 -- Session 2 UPDATE t SET IsSuccess = 2 WHERE id = 400000; -- 立即成功
分析: IsSuccess=2 索引記錄未被鎖,無衝突。
-- Session 1 SET autocommit=0; BEGIN; UPDATE t SET IsSuccess = 1 WHERE id = 400000; -- 鎖主鍵 id=400000 + 輔助索引 IsSuccess=1 -- 不 COMMIT -- Session 2 SELECT * FROM t WHERE getTime < 1 AND IsSuccess = 0 ORDER BY id LIMIT 30 FOR UPDATE; -- 阻塞!
分析: Session 2 掃描 IsSuccess=0 索引時,需確認 id=400000 當前是否仍為 0。因 Session 1 已鎖定該行主鍵並修改輔助索引值(未提交),Session 2 必須等待 Session 1 提交/回滾後,才能確定該行是否符合 IsSuccess=0 條件。
-- Session 1 BEGIN; UPDATE t SET IsSuccess = 1 WHERE id = 400000; -- 不提交 -- Session 2 SELECT * FROM t WHERE getTime < 1 AND IsSuccess = 0 ... FOR UPDATE; -- 阻塞 -- 但 WHERE getTime = 0 則不阻塞
結論: getTime < 1 範圍條件導致 Next-Key Lock 鎖定從開頭到 400000 的間隙;改為等值 getTime = 0 則僅鎖定精確記錄,避開衝突。
經典死鎖場景:
SELECT ... FOR UPDATE 先鎖主鍵索引 → 再鎖輔助索引UPDATE 先鎖輔助索引 → 再鎖主鍵索引SHOW ENGINE INNODB STATUS\G
LATEST DETECTED DEADLOCK 區塊顯示:兩事務持有/等待的鎖、SQL 文本、回滾事務 ID。
SHOW PROCESSLIST; KILL; -- 或 KILL QUERY 僅終止當前語句
SELECT * FROM information_schema.INNODB_TRX\G SELECT * FROM information_schema.INNODB_LOCKS\G SELECT * FROM information_schema.INNODB_LOCK_WAITS\G
<, >, BETWEEN)觸發 Next-Key Lock;改用等值或 IN 列表。READ COMMITTED(需配合 binlog_format=ROW),減少間隙鎖。FOR UPDATE SKIP LOCKED (MySQL 8.0+): 跳過已鎖行,適用佇列處理、分佈式鎖。Lock wait timeout exceeded / Deadlock found 異常,指數退避重試。InnoDB 鎖定索引而非物理行。FOR UPDATE 依條件與索引類型決定鎖定範圍,範圍條件易觸發 Next-Key Lock 鎖大區間。死鎖本質是多事務以不同順序請求相同索引鎖。掌握 SHOW ENGINE INNODB STATUS 分析、統一訪問順序、縮小掃描範圍、善用 SKIP LOCKED 与應用層重試,是解決高併發鎖衝突的核心武器庫。