資料庫隔離級別 (Transaction Isolation Level)

什麼是交易 (Transaction)?

在資料庫中,交易是指一組作為單一邏輯工作單元的資料庫操作。這些操作要麼全部成功,要麼全部失敗,以確保資料的完整性和一致性 (Atomicity)。

當多個交易同時對資料庫進行讀寫時,如果沒有適當的隔離機制,就可能引發資料不一致的問題。

三種常見的資料讀取問題

在高併發情境下,若不對交易進行隔離,可能出現以下三種常見的讀取問題:

  1. Dirty Read (髒讀)

    • 情境:交易 A 修改了一筆資料,但尚未提交 (commit),此時交易 B 讀取了這筆被修改過但未提交的資料。如果交易 A 最終因為某些原因而回滾 (rollback),那麼交易 B 讀到的就是一筆從未真正存在過的「髒」資料。
    • 比喻:老闆跟你說下個月要加薪,你很高興地告訴了家人。結果隔天老闆說公司財務有困難,加薪取消了。你家人得到的就是一個「髒」資訊。
  2. Non-Repeatable Read (不可重複讀)

    • 情境:在同一個交易內,兩次讀取同一筆資料,但返回的結果卻不同。這是因為在兩次讀取之間,有另一個交易修改了這筆資料並已提交。
    • 精準的說法:「在同一個交易內,多次讀取同一筆資料卻返回不同的值」。
    • 比喻:你第一次查機票價格是 5000 元,還在考慮時,航空公司調整了票價,你第二次查詢時,價格變成了 5500 元。
  3. 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):

  1. 針對目前交易 (Current Transaction) 如果你只需要在一個特定的交易中提高隔離級別(例如,在一個複雜的轉帳交易中),你可以這樣做:

    BEGIN;
    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    -- 在這裡執行你的 SQL 查詢...
    -- 這些查詢將會運作在 REPEATABLE READ 級別下
    COMMIT;
    -- 交易結束後,隔離級別會自動恢復成會話的預設值 (read committed)
    

    注意SET TRANSACTION 指令必須在交易的最開始、任何 SELECT, INSERT, UPDATE, DELETE 之前執行。

  2. 針對目前會話 (Current Session) 如果你希望目前連線中的所有後續交易都使用新的隔離級別,你可以設定會話的預設值:

    SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    

    設定之後,這個連線中所有新的交易都會預設使用 REPEATABLE READ,直到你再次更改它或斷開連線。

  3. 針對整個資料庫 (Database-wide) 如果你想更改整個資料庫的預設隔離級別,讓所有新的連線都使用新的級別,你可以使用 ALTER DATABASE。你必須擁有資料庫的超級使用者權限才能執行此操作。

    ALTER DATABASE your_database_name SET default_transaction_isolation = 'repeatable read';
    

    這個設定不會影響到目前已經建立的連線,只會對之後新建立的連線生效。

在活躍狀態下更改隔離級別會有影響嗎?

總結來說,更改隔離級別是安全的,但需要了解其影響

  • 不會中斷現有服務:當你使用 ALTER DATABASE 更改整個資料庫的預設隔離級別時,它不會影響任何正在執行中的交易或已經建立的連線。它只會改變新連線的預設行為。因此,在生產環境的活躍資料庫上執行這個指令,並不會造成服務中斷。

  • 主要的副作用是效能

    • 提高隔離級別 (例如從 Read Committed 改為 Repeatable ReadSerializable) 會增加資料庫的負擔。PostgreSQL 需要做更多的工作來追蹤資料版本和管理鎖定,這會導致:
      • 查詢延遲增加:部分查詢可能會變慢。
      • 併發能力下降:因為鎖定的範圍和時間可能變長,在高併發時更容易發生交易等待或衝突。
      • 更高的錯誤率:在 Serializable 級別下,如果偵測到可能破壞序列性的操作,交易會被強制中斷並回滾,回傳「serialization failure」錯誤。你的應用程式必須準備好捕捉這類錯誤並進行重試。
    • 降低隔離級別則會帶來相反的效果:效能提升,但資料一致性的風險增加。

總結建議

  • 除非有非常明確的需求,否則保持預設的 Read Committed 通常是最佳選擇。
  • 如果只有特定的業務邏輯需要更高的隔離性(例如產生重要報表、處理金融交易),請優先考慮在單一交易或會話中臨時提升隔離級別,而不是更改全域設定。
  • 如果要更改全域預設值,務必在測試環境中充分評估其對應用程式效能和穩定性的影響。

參考資料