深入理解死結(Deadlock)與發生條件
更新日期: 2025 年 3 月 4 日
本文為 SQL 關聯資料庫 基本介紹系列文,第 5 篇:
- 關聯式資料庫與資料完整性:初學者指南
- SQL 觸發器(Triggers):自動執行的資料庫機制
- Django 信號(Signals) vs SQL 觸發器(Triggers):關係與差異解析
- 關聯式資料庫與交易(Transaction)機制入門
- 深入理解死結(Deadlock)與發生條件 👈進度
- DCL(資料控制語言)入門:SQL 權限管理基礎
- SQL 儲存程序(Stored Procedure)入門
- ORM(對象關係對映):讓資料庫操作更簡單的工具
- SQL 儲存程序 vs ORM:如何選擇最適合的數據庫操作方式?
- 關聯式資料庫 View(檢視)是什麼?完整指南
- 理解 Materialized View:初學者指南
在電腦科學與資料庫管理中,「死結(Deadlock)」 是一種常見的問題,通常發生在多個執行緒或交易(Transaction)同時競爭資源時。
當一組交易彼此無限期地等待對方釋放資源,而這些資源又不會主動釋放時,就會發生死結。
舉個簡單的例子:
- 交易 A 鎖住
資源 X,並且在等待資源 Y。 - 交易 B 鎖住
資源 Y,但也在等待資源 X。 - 兩者互相等待對方釋放資源,導致無限等待的情況,這就是死結。
什麼是死結(Deadlock)?
死結的定義
死結(Deadlock)是指兩個或以上的程序(或交易)無限期地互相等待對方釋放資源,導致系統進入僵局,無法繼續執行。
死結的例子
範例 1️⃣:兩輛車在狹窄道路上互相阻擋
想像一下,兩輛車同時進入一條只能容納一輛車的狹窄巷道:
- 車 A 來自西邊,進入巷道後等待車 B 讓路。
- 車 B 來自東邊,進入巷道後等待車 A 讓路。
- 兩輛車都不願意倒車,也無法前進,導致交通堵塞,這就是死結。
範例 2️⃣:資料庫中的交易死結
考慮以下 SQL 交易:
-- 交易 A:先鎖住 accounts 表的 ID = 1,再試圖鎖住 ID = 2
BEGIN;
LOCK TABLE accounts IN EXCLUSIVE MODE WHERE id = 1;
LOCK TABLE accounts IN EXCLUSIVE MODE WHERE id = 2;
-- 交易 B:先鎖住 accounts 表的 ID = 2,再試圖鎖住 ID = 1
BEGIN;
LOCK TABLE accounts IN EXCLUSIVE MODE WHERE id = 2;
LOCK TABLE accounts IN EXCLUSIVE MODE WHERE id = 1;
此時:
- 交易 A 持有 ID = 1,等待 ID = 2
- 交易 B 持有 ID = 2,等待 ID = 1
- 兩者互相等待,導致死結。
graph TD;
A["交易 A"]
B["交易 B"]
X["資源 X (ID = 1)"]
Y["資源 Y (ID = 2)"]
A -->|鎖定| X
B -->|鎖定| Y
A -.->|等待| Y
B -.->|等待| X
死結的必要條件(Necessary Conditions)
要發生死結,系統必須滿足以下四個條件(如果其中任何一個條件不成立,死結就不會發生)。
| 條件 | 描述 |
|---|---|
| 互斥(Mutual Exclusion) | 資源一次只能被一個進程(或交易)持有,不能同時被多個使用者存取。 |
| 持有並等待(Hold and Wait) | 一個交易在持有某些資源的同時,還會繼續請求新的資源,而不釋放已持有的資源。 |
| 不可剝奪(No Preemption) | 資源不能被強制收回,只有持有該資源的交易可以自願釋放。 |
| 循環等待(Circular Wait) | 交易形成一個環狀的資源等待鏈,互相等待對方釋放資源。 |
互斥條件(Mutual Exclusion)
🔹 資源只能被一個交易使用,無法共享
這表示當某個交易或程序已經持有某個資源時,其他交易就不能同時使用這個資源,必須等它釋放後才能拿到。
📌 舉例:
假設有 一張 ATM 機,當 A 用戶 正在操作時,B 用戶 不能同時使用這台機器,必須等 A 完成操作後才能輪到 B。
持有並等待(Hold and Wait)
🔹 交易在持有資源的同時,還在等待其他資源
這表示一個交易已經鎖定了一部分資源,但它還想要更多資源,而這些資源可能已經被其他交易鎖住。
📌 舉例:
1️⃣ A(男生) 已經向身邊的朋友透露自己喜歡 B(女生)(持有「對 B 的感情」這個資源),但他不想自己先開口,希望 B 先告白(等待「B 的主動表白」這個資源)。
2️⃣ B(女生) 其實也喜歡 A,並且也向朋友表達過對 A 的好感(持有「對 A 的感情」這個資源),但她不想自己先開口,希望 A 先告白(等待「A 的主動表白」這個資源)。
3️⃣ 結果:
- A 不願意先告白,因為他還在等 B 先開口。
- B 也不願意先告白,因為她還在等 A 先開口。
- 雙方都已經擁有對彼此的愛意(持有資源),但誰也不願意放棄「等對方先開口」的機會(等待資源),結果雙方陷入長期曖昧,關係停滯不前,形成感情上的死結。
不可剝奪(No Preemption)
🔹 資源不能被強制奪走,只有持有資源的交易可以釋放
這表示當一個交易持有某個資源時,系統無法強制把資源拿走,只能等交易自己釋放。
📌 舉例:
- A 用戶 取款時,ATM 機內部已經開始處理交易,但突然 B 用戶急著用 ATM。
- 系統不會強制中斷 A 用戶的交易,然後讓 B 用戶先用,因為資源只能被持有者自己釋放。
- B 只能無限等待,直到 A 用戶操作結束,ATM 機才會空出來。
循環等待(Circular Wait)
🔹 交易之間形成「等待環」,互相等待對方釋放資源,導致無限卡住
這表示一組交易在環狀的等待關係中,每個交易都在等待另一個交易釋放資源,但沒有人能先釋放,最終所有交易都無限等待,導致死結。
📌 舉例:
- 交易 A 持有資源 X,並等待資源 Y
- 交易 B 持有資源 Y,並等待資源 X
- 交易 A 無法執行,因為 B 不釋放資源 Y;交易 B 也無法執行,因為 A 不釋放資源 X
- 這形成了一個環狀等待(Circular Wait),導致雙方都被卡住,進入死結。
死結的影響
- 系統無法繼續執行:所有涉及的交易都陷入等待狀態,導致應用程式無法正常運作。
- 效能下降:系統資源被鎖住,影響其他交易的執行,導致整體效能下降。
- 數據一致性風險:如果死結發生且沒有被適當處理,可能會導致數據不一致,甚至影響數據完整性。
預防死結的方法(如何避免死結發生?)
死結通常是因為交易互相等待對方釋放資源而產生,因此可以透過以下方法來降低發生死結的機率。
資源請求順序統一
當交易請求多個資源時,應該統一請求的順序,確保所有交易按照相同的順序來請求資源。這樣可以避免「循環等待(Circular Wait)」條件的產生。
📌 示例: 假設我們的系統有兩種資源:
- 資源 X(例如:客戶帳戶資料)
- 資源 Y(例如:交易記錄表)
交易請求的順序應該統一,例如:
- 交易 A 先請求
資源 X,再請求資源 Y - 交易 B 也必須先請求
資源 X,再請求資源 Y
🚫 錯誤情況(可能導致死結):
- 交易 A 先鎖住
資源 X,然後等待資源 Y - 交易 B 先鎖住
資源 Y,然後等待資源 X - 兩者形成「循環等待」,導致死結
✅ 正確做法(可避免死結):
- 所有交易都統一「先請求 X,再請求 Y」,這樣交易之間不會互相等待資源而卡住。
設置資源請求超時(Timeout)
當交易嘗試獲取資源時,應該設定最大等待時間,如果超過這個時間仍然無法獲取資源,則自動放棄並回滾交易,避免死結的發生。
📌 MySQL 設置交易鎖等待時間(10 秒)
SET innodb_lock_wait_timeout = 10;這表示:
- 若交易等待超過 10 秒仍無法獲取所需的資源,則自動回滾該交易,釋放已持有的資源,讓其他交易可以繼續執行。
這種方法可以確保系統不會因為某一筆交易長時間卡住資源,而影響其他交易的執行。
避免「持有並等待」(Hold and Wait)
當交易在請求新資源之前,應該先釋放已持有的資源,避免交易同時持有一部分資源,並且等待額外的資源,導致死結。
📌 正確做法(確保交易不會長時間持有資源)
BEGIN;
LOCK TABLE accounts IN EXCLUSIVE MODE WHERE id = 1;
COMMIT; -- 先釋放已鎖住的資源
BEGIN;
LOCK TABLE accounts IN EXCLUSIVE MODE WHERE id = 2;
COMMIT;
這樣可以確保:
- 交易在請求新資源前,已經釋放舊資源,減少交易長時間鎖住多個資源的機率。
- 避免「持有部分資源 + 等待其他資源」的情況,從而減少死結的發生。
如何解除死結?
當死結已經發生時,系統應該主動偵測並解除,否則交易會無限期地互相等待,導致系統效能下降或無法執行新的交易。
檢測死結並終止交易
大部分的資料庫管理系統(如 MySQL、PostgreSQL)內建死結偵測機制,當偵測到死結時,會強制回滾其中一個交易,讓其他交易能夠繼續執行。
📌 查詢 MySQL 當前的死結資訊
SHOW ENGINE INNODB STATUS;這條指令可以顯示:
- 當前發生的死結交易
- 哪些交易互相等待
- 交易佔用了哪些資源
✅ 如何解決?
- 讓系統自動回滾某些交易(通常是等待時間最長的交易)。
- 手動終止某個交易,讓其他交易繼續執行。
手動終止(KILL)特定交易
如果系統沒有自動解決死結,或者我們希望手動介入,則可以使用 KILL 指令來強制終止某個交易,讓其他交易能夠繼續執行。
📌 步驟 1:查詢當前正在執行的交易
SHOW PROCESSLIST;這條指令會顯示目前系統內正在執行的所有交易,包含:
id(交易 ID)user(執行交易的使用者)host(來源主機)db(影響的資料庫)command(執行中的指令)time(交易執行時間)state(交易狀態,如「Waiting for table lock」)
📌 步驟 2:終止特定交易
KILL <transaction_id>;假設我們發現交易 ID 為 1234 的交易導致死結,我們可以執行:
KILL 1234;這樣就能強制終止該交易,釋放其持有的資源,讓其他交易可以繼續執行。
結論
✅ 死結(Deadlock)發生時,交易互相等待對方釋放資源,導致無限期卡住,影響系統效能。
✅ 死結發生的必要條件有四個:互斥、持有並等待、不可剝奪、循環等待。只要打破其中一個條件,就能預防死結。
✅ 常見的預防策略包括設定資源請求順序、設置超時機制、優化資源管理等。
✅ 如果死結已發生,可透過資料庫的死結偵測機制、超時回滾或手動終止交易來解決。
| 方法 | 作用 | 適用於 |
|---|---|---|
| 資源請求順序統一 | 統一交易請求資源的順序,避免循環等待 | 預防死結 |
| 設置超時機制 | 設定最大等待時間,自動回滾超時的交易 | 預防死結 |
| 避免持有並等待 | 先釋放舊資源再請求新資源,減少交易長時間鎖住資源 | 預防死結 |
| 檢測死結並終止交易 | 系統自動偵測死結,並回滾其中一個交易 | 解決死結 |
| 手動終止特定交易 | 強制終止死結交易,釋放資源 | 解決死結 |
掌握這些概念後,你就能有效預防與解決死結問題,確保資料庫系統的穩定運行! 💡
