博客 / 詳情

返回

深入解析 InnoDB 死鎖:從案例到方案,全流程透視指南

大家好!今天我要和各位分享一個在 MySQL 項目中經常讓開發者頭疼的問題——InnoDB 的死鎖問題。相信不少朋友都遇到過這樣的情況:一個好好運行的系統突然報錯,日誌裏冒出"Deadlock found when trying to get lock; try restarting transaction",然後你就開始了漫長的排查之旅...

別擔心,這篇文章會用真實案例帶你從現象到根源,徹底掌握死鎖的排查技巧和解決方法。無論你是數據庫管理員還是後端開發,這些內容都能幫你在實際工作中少走彎路。

InnoDB 鎖機制基礎知識

在深入案例前,我們先用通俗的話聊聊 InnoDB 的鎖機制。

InnoDB 有幾種主要的鎖類型:

  • 共享鎖(S 鎖):大家一起看,不能改(SELECT ... LOCK IN SHARE MODE)
  • 排他鎖(X 鎖):我改數據時誰都別動(SELECT ... FOR UPDATE, UPDATE, DELETE)
  • 意向鎖(IS/IX 鎖):打個招呼説"我要在這張表的某些行上加鎖了"
  • 記錄鎖:鎖住單條記錄
  • 間隙鎖:鎖住一個範圍,但不包含記錄本身
  • 臨鍵鎖(Next-Key Lock):記錄鎖+間隙鎖的組合,防止幻讀的重要機制
  • 插入意向鎖:一種特殊的間隙鎖,表示插入操作的意向,多個事務可以在同一間隙中設置插入意向鎖而不衝突,但會與間隙鎖衝突

需要特別強調的是:間隙鎖和臨鍵鎖只在 REPEATABLE READ 隔離級別下默認生效。在 READ COMMITTED 隔離級別下,InnoDB 不使用間隙鎖,臨鍵鎖會退化為記錄鎖,這是減少死鎖的重要知識點。

想象一下,鎖就像是在圖書館看書。共享鎖就像大家一起看同一本書但不能寫批註,排他鎖就像你借走了這本書,別人就看不了了。

什麼是死鎖?

死鎖就像兩個人互相給對方讓路,結果誰都動不了的尷尬局面。在數據庫中,當兩個或多個事務互相等待對方釋放鎖,導致所有事務都無法繼續執行時,就形成了死鎖。

舉個簡單例子:小明要拿筷子和碗才能吃飯,小紅也一樣。現在小明拿了筷子,小紅拿了碗,兩人都在等對方放下手中的東西,結果誰都吃不了飯。

graph LR
    A[事務1] -->|持有| B[資源A]
    A -->|請求| C[資源B]
    D[事務2] -->|持有| C
    D -->|請求| B

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style D fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#bbf,stroke:#333,stroke-width:2px

案例一:經典的行鎖更新死鎖

場景描述

假設我們有一個訂單系統,有一張orders表,結構如下:

CREATE TABLE `orders` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `product_id` int NOT NULL,
  `status` int NOT NULL,
  `amount` decimal(10,2) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB;

系統中存在兩個併發執行的事務,導致了死鎖:

  • 事務 A:更新用户 1 的所有訂單狀態
  • 事務 B:更新特定產品相關的所有訂單金額

問題重現

以下是導致死鎖的操作序列:

這裏需要注意一個重要點:InnoDB 的行鎖是通過索引實現的。在這個案例中,事務 A 使用user_id索引,事務 B 使用product_id索引,導致鎖定的記錄順序不同,增加了死鎖風險。如果查詢條件未命中索引,情況會更糟,可能導致表鎖或大範圍的臨鍵鎖。

死鎖日誌分析

當死鎖發生時,MySQL 會在錯誤日誌中記錄詳細信息。使用SHOW ENGINE INNODB STATUS可以查看最近一次死鎖的詳情:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 14:32:51 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10795, ACTIVE 2 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 155, OS thread handle 140301189614336, query id 9697 localhost root updating
UPDATE orders SET status=2 WHERE user_id=1 AND id>2

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10795 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 6; hex 000000002a25; asc     *%;;
 2: len 7; hex 81000000110137; asc       7;;
 3: len 4; hex 80000002; asc     ;;
 4: len 4; hex 80000065; asc    e;;
 5: len 4; hex 80000001; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 10794, ACTIVE 5 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 156, OS thread handle 140301235861248, query id 9696 localhost root updating
UPDATE orders SET amount=amount*1.1 WHERE product_id=101 LIMIT 1 OFFSET 1

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10794 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 6; hex 000000002a25; asc     *%;;
 2: len 7; hex 81000000110137; asc       7;;
 3: len 4; hex 80000002; asc     ;;
 4: len 4; hex 80000065; asc    e;;
 5: len 4; hex 80000001; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10794 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000002a23; asc     *#;;
 2: len 7; hex 81000000110135; asc       5;;
 3: len 4; hex 80000001; asc     ;;
 4: len 4; hex 80000065; asc    e;;
 5: len 4; hex 80000001; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

從日誌中我們可以看到:

  1. 事務 A(TRANSACTION 10795)想獲取 id=3 的行鎖,但被事務 B 持有
  2. 同時事務 B(TRANSACTION 10794)想獲取 id=1 的行鎖,但被事務 A 持有
  3. 形成循環等待,MySQL 檢測到死鎖並回滾了事務 A

日誌中一些專業術語解釋:

  • lock_mode X:代表排他鎖(X 鎖)
  • locks rec but not gap:表示只鎖記錄,不鎖間隙(記錄鎖)
  • heap no:表示記錄在索引頁中的位置

解決方案

對於這種情況,以下是按實用性排序的解決方法:

  1. 統一訪問順序(最有效):確保所有事務按相同的順序訪問記錄,例如總是按主鍵順序
-- 事務A
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 ORDER BY id;
COMMIT;

-- 事務B
BEGIN;
UPDATE orders SET amount=amount*1.1 WHERE product_id=101 ORDER BY id;
COMMIT;
  1. 減小事務範圍:不要在一個大事務中做太多事情,拆分為多個小事務
-- 原來的大事務
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1;
-- 其他操作...
COMMIT;

-- 拆分後
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 AND id BETWEEN 1 AND 100;
COMMIT;

BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 AND id BETWEEN 101 AND 200;
COMMIT;
  1. 添加適當的鎖超時設置
SET innodb_lock_wait_timeout = 50; -- 設置鎖等待超時
  1. 使用樂觀鎖替代悲觀鎖(適合讀多寫少場景):
-- 使用版本號控制
UPDATE orders SET amount=amount*1.1, version=version+1
WHERE product_id=101 AND version=當前版本;

需要注意的是,樂觀鎖在併發衝突頻繁的場景下可能導致大量重試,反而降低性能。此時應考慮悲觀鎖加合理的鎖超時設置。

案例二:Gap 鎖導致的死鎖

場景描述

在 REPEATABLE READ 隔離級別下,InnoDB 會使用間隙鎖(Gap Lock)來防止幻讀。這種鎖可能導致一些不直觀的死鎖。

假設有一個用户積分表:

CREATE TABLE `user_points` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `points` int NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB;

表中已有數據:

id | user_id | points | created_at
1  | 1       | 100    | 2023-04-15 10:00:00
3  | 3       | 150    | 2023-04-15 11:00:00
5  | 5       | 200    | 2023-04-15 12:00:00

注意這裏 user_id 為 2 和 4 的記錄不存在。

問題重現

graph LR
    A[事務A] -->|持有| C["Gap鎖(1,3)"]
    A -->|請求| E["插入意向鎖(4)"]

    B[事務B] -->|持有| D["Gap鎖(3,5)"]
    B -->|請求| F["插入意向鎖(2)"]

    C -.->|阻塞| F
    D -.->|阻塞| E

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#f9f,stroke:#333,stroke-width:2px
    style C fill:#bbf,stroke:#333,stroke-width:2px
    style D fill:#bbf,stroke:#333,stroke-width:2px
    style E fill:#bfb,stroke:#333,stroke-width:2px
    style F fill:#bfb,stroke:#333,stroke-width:2px

這裏的關鍵是理解:在 RR 隔離級別下,即使查詢的記錄不存在,FOR UPDATE也會在該位置獲取間隙鎖或臨鍵鎖。當執行 INSERT 操作時,事務會先請求一個插入意向鎖(Insert Intention Lock),這是一種特殊的間隙鎖。雖然多個事務可以在同一個間隙內持有不同的插入意向鎖(允許併發插入不同位置),但插入意向鎖會與普通間隙鎖衝突,導致等待。

當兩個事務交叉持有間隙鎖並嘗試插入時,就會形成死鎖。

臨鍵鎖案例補充

為更好理解臨鍵鎖,考慮以下場景:

-- 在REPEATABLE READ隔離級別下
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 5 AND 15 FOR UPDATE;
-- 其他操作...
COMMIT;

這個 SELECT 語句會做什麼?它會:

  1. 對 id 值為 5 到 15 的記錄加記錄鎖(Record Lock)
  2. 對 id 值範圍(15, "下一個索引值")加間隙鎖(Gap Lock)
  3. 這兩種鎖合起來形成臨鍵鎖(Next-Key Lock)

這種鎖定策略可以防止其他事務在鎖定範圍內插入新記錄(防幻讀),同時允許對鎖定範圍之外的記錄進行修改。

死鎖日誌分析

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 15:12:45 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10801, ACTIVE 6 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 157, OS thread handle 140301189614336, query id 9712 localhost root update
INSERT INTO user_points(user_id,points,created_at) VALUES(4,120,NOW())

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 4 n bits 72 index idx_user_id of table `test`.`user_points` trx id 10801 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000005; asc     ;;
 1: len 4; hex 80000005; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 10802, ACTIVE 3 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 158, OS thread handle 140301235861248, query id 9713 localhost root update
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW())

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 4 n bits 72 index idx_user_id of table `test`.`user_points` trx id 10802 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 4; hex 80000003; asc     ;;

*** WE ROLL BACK TRANSACTION (2)

日誌中的關鍵信息:

  • lock_mode X locks gap before rec insert intention waiting:表示事務嘗試獲取插入意向鎖,但被另一個事務的間隙鎖阻止
  • 兩個事務分別持有對方需要的間隙鎖,形成死鎖

解決方案

按照有效性排序:

  1. 降低隔離級別(最直接有效):將隔離級別從 REPEATABLE READ 降低到 READ COMMITTED,這樣就不會使用 Gap 鎖
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

注意:隔離級別調整需謹慎評估業務一致性需求,避免幻讀問題對應用造成影響。

  1. 使用 INSERT ON DUPLICATE KEY UPDATE:替代先 SELECT 再 INSERT 的模式
INSERT INTO user_points(user_id,points,created_at)
VALUES(2,110,NOW())
ON DUPLICATE KEY UPDATE points=110, created_at=NOW();
  1. 拆分事務:不要在同一個事務中先查詢再插入
-- 原來的模式
BEGIN;
SELECT * FROM user_points WHERE user_id=2 FOR UPDATE;
-- 如果不存在則插入
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW());
COMMIT;

-- 改進後
-- 查詢階段(可以使用共享鎖或不加鎖)
SELECT * FROM user_points WHERE user_id=2;

-- 插入階段(單獨事務)
BEGIN;
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW())
ON DUPLICATE KEY UPDATE points=110, created_at=NOW();
COMMIT;
  1. 索引優化:確保查詢條件有合適的索引

案例三:外鍵約束導致的死鎖

場景描述

外鍵約束也是死鎖的常見原因。考慮以下兩個表:

CREATE TABLE `departments` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE TABLE `employees` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `dept_id` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_dept_id` (`dept_id`),
  CONSTRAINT `fk_dept_id` FOREIGN KEY (`dept_id`) REFERENCES `departments` (`id`)
) ENGINE=InnoDB;

問題重現

這裏的關鍵點:外鍵約束會導致額外的鎖請求。當我們修改引用字段時,InnoDB 需要驗證引用的完整性:

  1. 如果更新子表的外鍵值(如 dept_id),InnoDB 會檢查父表(departments)中對應的值是否存在,需要在父表相應記錄上添加共享鎖(S 鎖),而非排他鎖
  2. 如果更新或刪除父表中被引用的記錄,InnoDB 會檢查子表是否有依賴,可能添加父子表間的額外鎖

這些額外的鎖請求大大增加了死鎖的可能性。

死鎖日誌分析

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 16:03:11 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10809, ACTIVE 8 sec updating or deleting
mysql tables in use 2, locked 2
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 159, OS thread handle 140301189614336, query id 9725 localhost root updating
UPDATE employees SET dept_id=2 WHERE id=1

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `test`.`employees` trx id 10809 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000002a3d; asc     *=;;
 2: len 7; hex 81000000110110; asc       ;;
 3: len 3; hex 426f62; asc Bob;;
 4: len 4; hex 80000001; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 10810, ACTIVE 4 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 160, OS thread handle 140301235861248, query id 9726 localhost root update
INSERT INTO departments(id,name) VALUES(3,'HR')

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`departments` trx id 10810 lock_mode X locks rec but not gap waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** WE ROLL BACK TRANSACTION (2)

解決方案

按照有效性排序:

  1. 調整事務順序(最有效):按照固定順序訪問相關表
-- 統一先操作父表,再操作子表
BEGIN;
UPDATE departments SET name='IT' WHERE id=1;
UPDATE employees SET dept_id=2 WHERE id=1;
COMMIT;
  1. 設置外鍵約束時使用RESTRICT而非CASCADE
ALTER TABLE employees DROP FOREIGN KEY fk_dept_id;
ALTER TABLE employees ADD CONSTRAINT fk_dept_id FOREIGN KEY (dept_id)
REFERENCES departments(id) ON DELETE RESTRICT ON UPDATE RESTRICT;
  1. 分割事務:避免在一個事務中同時操作多個相關表
-- 分兩個事務操作
-- 事務1:更新父表
BEGIN;
UPDATE departments SET name='IT' WHERE id=1;
COMMIT;

-- 事務2:更新子表
BEGIN;
UPDATE employees SET dept_id=2 WHERE id=1;
COMMIT;
  1. 減少外鍵使用(謹慎考慮):在高併發系統中考慮減少外鍵約束,由應用程序保證數據一致性

案例四:自增鎖死鎖

場景描述

自增鎖是一種特殊的鎖,用於處理 AUTO_INCREMENT 列的值生成。在高併發場景下,自增鎖爭用也可能導致死鎖。

MySQL 通過innodb_autoinc_lock_mode參數控制自增鎖行為:

  • innodb_autoinc_lock_mode=0:傳統模式,所有插入語句都需要獲取表級鎖
  • innodb_autoinc_lock_mode=1(默認值):混合模式,其特點是:

    • 對於行數已知的插入(如INSERT ... VALUES),使用輕量級互斥鎖
    • 僅對於行數未知的插入(如INSERT ... SELECT),才使用表級鎖
  • innodb_autoinc_lock_mode=2:交叉模式,所有插入使用輕量級互斥鎖,不保證自增值連續性

問題重現

考慮以下場景:

CREATE TABLE `order_items` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_id` int NOT NULL,
  `product_id` int NOT NULL,
  `quantity` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB;

這裏的關鍵點是理解:在innodb_autoinc_lock_mode=1(默認)模式下,INSERT ... SELECT等行數不確定的語句會獲取表級自增鎖,而INSERT ... VALUES等行數確定的語句僅獲取輕量級互斥鎖。當兩種類型的插入混合使用時,容易導致鎖衝突和死鎖。

解決方案

  1. 調整自增鎖模式(最有效):
-- 全局設置,需要重啓MySQL生效
SET GLOBAL innodb_autoinc_lock_mode = 2;

-- 或在配置文件中設置
[mysqld]
innodb_autoinc_lock_mode = 2

注意:mode=2不保證自增值連續性,但能顯著減少鎖衝突。

  1. 批量插入優化:儘量在一個語句中插入多行數據,明確指定字段順序
-- 替代多次單行插入
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (101, 1, 2), (101, 2, 1), (101, 3, 5);
  1. 使用應用生成的 ID:對於高併發系統,考慮使用應用層生成唯一 ID
-- 使用應用生成的UUID或其他唯一ID
INSERT INTO order_items (id, order_id, product_id, quantity)
VALUES (GENERATED_ID, 101, 1, 2);

死鎖排查工具與技巧

1. 使用 SHOW ENGINE INNODB STATUS 查看死鎖信息

SHOW ENGINE INNODB STATUS\G

關注輸出中的"LATEST DETECTED DEADLOCK"部分。

2. 開啓死鎖日誌記錄

SET GLOBAL innodb_print_all_deadlocks = 1;

這會將所有死鎖信息記錄到 MySQL 錯誤日誌中。

3. 使用 performance_schema 監控鎖等待

-- 啓用performance_schema
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'wait/lock/metadata/%' OR NAME LIKE 'wait/lock/innodb/%';

-- 查詢當前鎖等待
SELECT * FROM performance_schema.events_waits_current
WHERE EVENT_NAME LIKE 'wait/lock%';

-- 查詢鎖等待歷史
SELECT * FROM performance_schema.events_waits_history
WHERE EVENT_NAME LIKE 'wait/lock%';

4. 使用工具分析死鎖

  • pt-deadlock-logger(Percona 工具集)
  • MySQL 企業版監控工具

5. 死鎖監控與預警

graph TD
    A[設置死鎖監控] --> B[開啓死鎖日誌]
    B --> C[編寫腳本定期分析日誌]
    C --> D[設置告警閾值]
    D --> E[超過閾值發送告警]
    E --> F[立即排查問題]

其他常見死鎖類型簡述

元數據鎖死鎖

當 DDL 操作(如 ALTER TABLE)與 DML 操作(如 INSERT、UPDATE)併發執行時,可能出現元數據鎖死鎖。

解決方案

  • 將 DDL 操作安排在低峯期
  • 使用在線 DDL 工具(如 pt-online-schema-change):這些工具通過創建臨時表、複製數據和表結構交換來避免長時間鎖表
  • MySQL 5.6+的ALGORITHM=INPLACE或 8.0+的ALGORITHM=INSTANT參數可實現某些 DDL 操作的在線執行
  • 避免長事務與 DDL 併發

預防死鎖的實用建議

  1. 控制事務大小和持續時間

    • 保持事務短小、快速完成
    • 只在必要時使用事務
  2. 合理設計數據訪問順序

    • 按照主鍵或索引順序訪問數據
    • 使用 ORDER BY 確保訪問順序一致
  3. 選擇合適的隔離級別

    • 不需要可重複讀的場景使用 READ COMMITTED
    • 瞭解每個隔離級別的鎖行為
  4. 優化索引設計

    • 確保查詢條件有合適的索引
    • 避免使用不必要的鎖定查詢
  5. 謹慎使用外鍵

    • 高併發系統可考慮減少外鍵約束
    • 使用 RESTRICT 代替 CASCADE
  6. 應用層重試機制

    • 捕獲死鎖異常並實現重試邏輯
int retries = 3;
boolean success = false;
while (retries > 0 && !success) {
    Connection conn = null;
    try {
        conn = dataSource.getConnection();
        conn.setAutoCommit(false);  // 開啓事務

        // 數據庫操作
        PreparedStatement ps = conn.prepareStatement("UPDATE...");
        ps.executeUpdate();

        conn.commit();  // 提交事務
        success = true;
    } catch (SQLException e) {
        if (conn != null) {
            try {
                conn.rollback();  // 回滾事務
            } catch (SQLException ex) {
                // 處理回滾異常
            }
        }

        if (e.getErrorCode() == 1213 && retries > 1) { // MySQL死鎖錯誤碼1213
            retries--;
            Thread.sleep(100); // 短暫延遲後重試
        } else {
            throw e; // 重試失敗或其他錯誤,繼續拋出
        }
    } finally {
        if (conn != null) {
            conn.close();
        }
    }
}
  1. 使用樂觀鎖替代悲觀鎖

    • 適合讀多寫少的場景
    • 使用版本號或時間戳實現
    • 注意:高衝突場景下樂觀鎖可能導致頻繁重試

死鎖排查流程

flowchart TD
    A[發現死鎖錯誤] --> B{是否能重現?}
    B -->|能| C[分析復現步驟]
    B -->|不能| D[查看死鎖日誌]
    C --> E[識別鎖類型與資源]
    D --> E
    E --> F[分析事務訪問順序]
    F --> G[確認死鎖原因]
    G --> H{原因類型?}
    H -->|訪問順序| I[統一訪問順序]
    H -->|鎖範圍| J[調整隔離級別/鎖範圍]
    H -->|事務設計| K[優化事務設計]
    H -->|外鍵約束| L[調整外鍵策略]
    I --> M[驗證解決方案]
    J --> M
    K --> M
    L --> M
    M --> N[監控並預防]

總結

死鎖類型 典型特徵 解決方案 開發成本 適用場景
行鎖更新衝突 多個事務更新相同或相關行數據 統一訪問順序、減小事務範圍、使用樂觀鎖 高併發多表更新業務
Gap 鎖衝突 REPEATABLE READ 隔離級別下插入操作死鎖 降低隔離級別、使用 ON DUPLICATE KEY UPDATE 需要頻繁插入查詢的應用
外鍵約束死鎖 父子表併發操作導致鎖衝突 減少外鍵使用、調整事務順序、使用 RESTRICT 約束 中高 具有複雜關係模型的系統
元數據鎖死鎖 DDL 和 DML 語句混合執行 將 DDL 操作放在低峯期、使用在線 DDL 工具 需要頻繁架構變更的應用
自增鎖死鎖 多事務同時插入自增列 調整 innodb_autoinc_lock_mode、批量插入 高併發寫入場景

通過本文的案例和分析,相信你已經對 InnoDB 死鎖有了更深入的理解。記住,死鎖不可完全避免,但可以通過合理的設計和實踐大大減少其發生頻率和影響。當死鎖發生時,保持冷靜,按照本文提供的排查流程,你一定能夠找到問題所在並解決它。


感謝您耐心閲讀到這裏!如果覺得本文對您有幫助,歡迎點贊 👍、收藏 ⭐、分享給需要的朋友,您的支持是我持續輸出技術乾貨的最大動力!

如果想獲取更多 Java 技術深度解析,歡迎點擊頭像關注我,後續會每日更新高質量技術文章,陪您一起進階成長~

user avatar u_16694542 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.