批量更新場景下,因不同事務以不同順序鎖定行記錄,極易觸發死鎖。本文通過實戰案例深度剖析死鎖產生機制,提供「統一排序」這一通用解決方案。
-- 業務 SQL:批量更新營銷活動頁面瀏覽量
UPDATE cross_marketing
SET gmtModified = NOW(), pageview = pageview + #{extpageview}
WHERE marketingId = #{marketingId};
並發場景:
marketingId = 1, 3, 5, 12marketingId = 1, 2, 5, 3時間軸 →
事務 A: 鎖行(1) → 鎖行(3) → 等待鎖行(5) [被 B 持有]
事務 B: 鎖行(1) → 鎖行(2) → 鎖行(5) → 等待鎖行(3) [被 A 持有]
↑ ↑
持有鎖(3) 等待(5) 持有鎖(5) 等待(3)
循環等待 = 死鎖
MySQL InnoDB 檢測到死鎖後,會選擇代價較小的事務回滾(通常是修改行數較少者),返回 ERROR 1213 (HY000): Deadlock found when trying to get lock; try restarting transaction。
滿足 Coffman 條件中的「互斥、請求與保持、不剝佔、循環等待」四條件即發生死鎖。
核心原則: 所有並發事務以**相同順序**訪問同一組資源(按主鍵/唯一鍵升序)。
-- 修正前(應用層隨機順序)
List ids = Arrays.asList(1, 3, 5, 12); // 線程 A
List ids = Arrays.asList(1, 2, 5, 3); // 線程 B
-- 修正後(統一排序)
List ids = Arrays.asList(1, 3, 5, 12); // 線程 A
List ids = Arrays.asList(1, 2, 3, 5); // 線程 B 排序後
-- Java 實現
ids.sort(Long::compareTo); // 或 Collections.sort(ids)
for (Long id : ids) {
updateMarketing(id, extPageview);
}
| 策略 | 適用場景 | 實現要點 |
|---|---|---|
| 應用層排序 | 批量更新/刪除 ID 可控 | ID 列表排序後逐條或批次執行 |
| 單條 SQL IN 列表 | ID 列表較短(< 1000) | WHERE id IN (1,2,3,5) MySQL 內部會按索引順序掃描 |
| 分批處理 | 大批量(> 10000) | 分批 500-1000 條,每批提交,減少持鎖時間 |
| SELECT ... FOR UPDATE 排序 | 需先查後改 | SELECT * FROM t WHERE id IN (...) ORDER BY id FOR UPDATE |
| 應用層分佈式鎖 | 跨服務/跨實例 | Redisson/Redis 鎖,粒度按 business_key |
-- 1. 查看最近死鎖詳情
SHOW ENGINE INNODB STATUS\G
-- 關注 LATEST DETECTED DEADLOCK 區塊
-- 2. 開啟死鎖記錄(MySQL 5.7+)
innodb_print_all_deadlocks=ON
-- 錯誤日誌將記錄完整死鎖資訊
-- 3. 監控鎖等待
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
-- 4. 實時查看事務
SELECT * FROM information_schema.INNODB_TRX\G
SELECT * FROM information_schema.INNODB_LOCKS\G
SELECT * FROM information_schema.INNODB_LOCK_WAITS\G
innodb_lock_wait_timeout(建議 5-10s)innodb_print_all_deadlocks=ONLock_deadlocks 計數器,設定告警