資料庫隔離級別 (Transaction Isolation Level)
什麼是交易 (Transaction)?
在資料庫中,交易是指一組作為單一邏輯工作單元的資料庫操作。這些操作要麼全部成功,要麼全部失敗,以確保資料的完整性和一致性 (Atomicity)。
當多個交易同時對資料庫進行讀寫時,如果沒有適當的隔離機制,就可能引發資料不一致的問題。
三種常見的資料讀取問題
在高併發情境下,若不對交易進行隔離,可能出現以下三種常見的讀取問題:
-
Dirty Read (髒讀)
- 情境:交易 A 修改了一筆資料,但尚未提交 (commit),此時交易 B 讀取了這筆被修改過但未提交的資料。如果交易 A 最終因為某些原因而回滾 (rollback),那麼交易 B 讀到的就是一筆從未真正存在過的「髒」資料。
- 比喻:老闆跟你說下個月要加薪,你很高興地告訴了家人。結果隔天老闆說公司財務有困難,加薪取消了。你家人得到的就是一個「髒」資訊。
-
Non-Repeatable Read (不可重複讀)
- 情境:在同一個交易內,兩次讀取同一筆資料,但返回的結果卻不同。這是因為在兩次讀取之間,有另一個交易修改了這筆資料並已提交。
- 精準的說法:「在同一個交易內,多次讀取同一筆資料卻返回不同的值」。
- 比喻:你第一次查機票價格是 5000 元,還在考慮時,航空公司調整了票價,你第二次查詢時,價格變成了 5500 元。
-
Phantom Read (幻讀)
- 情境:在同一個交易內,兩次執行相同的範圍查詢 (range query),但第二次查詢返回的結果集包含了第一次查詢時不存在的「幻影」資料列。這是因為在兩次查詢之間,有另一個交易新增或刪除了符合查詢條件的資料。
- 與 Non-Repeatable Read 的區別:不可重複讀是針對「單一資料列」的修改,而幻讀是針對「多筆資料列」的新增或刪除。
- 比喻:你第一次統計你們部門有 10 個人,在你統計的過程中,人資部門新招了一位員工並加到你們部門,你再次統計時,發現變成了 11 個人。
四種隔離級別 (Isolation Levels)
為了解決上述問題,SQL 標準定義了四種隔離級別,隔離強度由低到高排列:
| 隔離級別 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | 可能發生 | 可能發生 | 可能發生 |
| Read Committed | 不會發生 | 可能發生 | 可能發生 |
| Repeatable Read | 不會發生 | 不會發生 | 可能發生 |
| Serializable | 不會發生 | 不會發生 | 不會發生 |
1. Read Uncommitted (讀取未提交)
- 特性:最低的隔離級別,允許交易讀取其他交易尚未提交的變更。效能最好,但資料一致性最差。
- 解決問題:無。
- 使用情境:極少使用。適用於對資料一致性要求極低,可以接受讀到「髒」資料的場景,例如監控系統的非精確數據統計。
2. Read Committed (讀取已提交)
- 特性:保證一個交易只能讀取到其他交易已經提交的資料,解決了髒讀問題。這是大多數資料庫(如 PostgreSQL, SQL Server, Oracle)預設的隔離級別。
- 解決問題:Dirty Read。
- 使用情境:適用於大多數的一般應用。在這種級別下,可以避免讀到無效資料,但對於需要多次讀取同一資料並保證結果一致的報表或複雜查詢,可能就不夠用。
3. Repeatable Read (可重複讀)
- 特性:保證在同一個交易中,多次讀取同一筆資料的結果都是一樣的,解決了不可重複讀的問題。MySQL (InnoDB) 的預設隔離級別。
- 解決問題:Dirty Read, Non-Repeatable Read。
- 使用情境:當應用需要確保在一一個交易過程中,所依賴的資料不會被其他交易修改時。例如,一個需要先讀取帳戶餘額,然後根據餘額進行一系列計算和更新的金融交易。如果餘額在計算過程中被改變,可能導致錯誤的結果。
4. Serializable (可序列化)
- 特性:最高的隔離級別,透過鎖定機制,強制交易順序執行,完全避免了上述所有讀取問題,如同所有交易都是一個接一個地執行。效能最差,但資料一致性最好。
- 解決問題:Dirty Read, Non-Repeatable Read, Phantom Read。
- 使用情境:對資料一致性要求極高的場景。例如,在需要確保資料絕對準確無誤的金融系統或庫存管理系統中。如果一個操作需要讀取庫存,計算後更新,那麼在整個過程中,絕對不能有其他交易來修改庫存數量,否則會導致超賣或庫存數據混亂。
如何選擇隔離級別?
選擇隔離級別需要在「資料一致性」和「系統效能」之間做出權衡:
- 隔離級別越高:資料越一致、越安全,但需要更多的鎖定機制,導致資料庫併發效能越低。
- 隔離級別越低:資料庫併發效能越高,但資料一致性的風險也越大。
總結建議:
- 從
Read Committed開始:這是大部分資料庫的預設值,能滿足多數應用場景的需求。 - 當心
Repeatable Read:如果你的應用在一個交易中需要多次讀取同一筆資料,且結果必須一致,可以考慮使用Repeatable Read,但要注意 MySQL 因為 gap lock 機制,可能會增加死鎖 (deadlock) 的風險。 - 謹慎使用
Serializable:除非你的業務對資料一致性有極端嚴格的要求,且可以接受效能上的犧牲,否則應避免使用Serializable。
PostgreSQL 實戰範例
在 PostgreSQL 中,隔離級別是一個交易 (Transaction) 層級的設定,而不是針對單一資料表。你可以為整個資料庫設定預設的隔離級別,也可以在需要時為單一的交易或會話 (Session) 臨時指定不同的級別。
如何取得目前的隔離級別?
你可以使用 SHOW 指令來查看當前會話預設的隔離級別。
SHOW transaction_isolation;
執行後,你會得到類似下面的結果,這代表 PostgreSQL 的預設隔離級別是 Read committed。
transaction_isolation
-----------------------
read committed
(1 row)
如何更新隔離級別?
更新隔離級別有三種不同的範圍 (Scope):
-
針對目前交易 (Current Transaction) 如果你只需要在一個特定的交易中提高隔離級別(例如,在一個複雜的轉帳交易中),你可以這樣做:
BEGIN; SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 在這裡執行你的 SQL 查詢... -- 這些查詢將會運作在 REPEATABLE READ 級別下 COMMIT; -- 交易結束後,隔離級別會自動恢復成會話的預設值 (read committed)注意:
SET TRANSACTION指令必須在交易的最開始、任何SELECT,INSERT,UPDATE,DELETE之前執行。 -
針對目前會話 (Current Session) 如果你希望目前連線中的所有後續交易都使用新的隔離級別,你可以設定會話的預設值:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;設定之後,這個連線中所有新的交易都會預設使用
REPEATABLE READ,直到你再次更改它或斷開連線。 -
針對整個資料庫 (Database-wide) 如果你想更改整個資料庫的預設隔離級別,讓所有新的連線都使用新的級別,你可以使用
ALTER DATABASE。你必須擁有資料庫的超級使用者權限才能執行此操作。ALTER DATABASE your_database_name SET default_transaction_isolation = 'repeatable read';這個設定不會影響到目前已經建立的連線,只會對之後新建立的連線生效。
在活躍狀態下更改隔離級別會有影響嗎?
總結來說,更改隔離級別是安全的,但需要了解其影響。
-
不會中斷現有服務:當你使用
ALTER DATABASE更改整個資料庫的預設隔離級別時,它不會影響任何正在執行中的交易或已經建立的連線。它只會改變新連線的預設行為。因此,在生產環境的活躍資料庫上執行這個指令,並不會造成服務中斷。 -
主要的副作用是效能:
- 提高隔離級別 (例如從
Read Committed改為Repeatable Read或Serializable) 會增加資料庫的負擔。PostgreSQL 需要做更多的工作來追蹤資料版本和管理鎖定,這會導致:- 查詢延遲增加:部分查詢可能會變慢。
- 併發能力下降:因為鎖定的範圍和時間可能變長,在高併發時更容易發生交易等待或衝突。
- 更高的錯誤率:在
Serializable級別下,如果偵測到可能破壞序列性的操作,交易會被強制中斷並回滾,回傳「serialization failure」錯誤。你的應用程式必須準備好捕捉這類錯誤並進行重試。
- 降低隔離級別則會帶來相反的效果:效能提升,但資料一致性的風險增加。
- 提高隔離級別 (例如從
總結建議:
- 除非有非常明確的需求,否則保持預設的
Read Committed通常是最佳選擇。 - 如果只有特定的業務邏輯需要更高的隔離性(例如產生重要報表、處理金融交易),請優先考慮在單一交易或會話中臨時提升隔離級別,而不是更改全域設定。
- 如果要更改全域預設值,務必在測試環境中充分評估其對應用程式效能和穩定性的影響。