CodeGym /課程 /SQL SELF /執行交易時的異常狀況: Dirty Read、 Non-Repeatab...

執行交易時的異常狀況: Dirty Read、 Non-Repeatable Read、 Phantom Read

SQL SELF
等級 40, 課堂 3
開放

我們再來複習一下交易的類型吧!在資料庫交易的世界裡,有三個主要的「壞蛋」會讓你很頭痛:Dirty Read、Non-Repeatable Read 跟 Phantom Read。這些「異常」都是因為交易隔離等級不夠高才會出現。今天就來聊聊這些壞蛋是誰、怎麼出現的,還有——最重要的——怎麼對付它們。

在看例子之前,先來回憶一下這些異常是什麼意思。

  • Dirty Read(髒讀):
    你讀到的資料已經被改過,但那個改資料的交易還沒 commit(COMMIT),甚至可能會被 rollback(ROLLBACK)。就像你把錢轉給朋友,看餘額發現自己破產了,但後來又反悔把錢拿回來。超神奇吧!

  • Non-Repeatable Read(不可重複讀):
    你在同一個交易裡讀同一筆資料兩次,但中間有別的交易把資料改掉了,所以你看到兩個不同的結果。就像你看護照上的生日,然後把護照給朋友,他偷偷改了數字,你再看一次,生日就變了。

  • Phantom Read(幽靈讀):
    你執行同一個查詢兩次,第二次卻多了其他交易新加進來的資料列。就像你數房間裡的人數,有人偷偷帶朋友進來,你再數一次,人變多了。

Dirty Read 的問題

假設我們有一個 accounts 資料表:

CREATE TABLE accounts (
    account_id SERIAL PRIMARY KEY,
    owner TEXT NOT NULL,
    balance NUMERIC(10, 2) NOT NULL
);
INSERT INTO accounts (owner, balance) VALUES ('Alice', 1000), ('Bob', 500);

交易 1 改了餘額但還沒結束,交易 2 同時想讀這個資料。

交易 1:

BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE owner = 'Alice';
-- Alice 的餘額變成 800,但交易還沒結束。

交易 2:

BEGIN;
SELECT balance FROM accounts WHERE owner = 'Alice'; -- 看到餘額:800(髒讀)。
ROLLBACK; -- 交易 1 被回滾。

現在交易 2 用到的資料其實是錯的,因為交易 1 把改動取消了。這種情況可以用 READ COMMITTED 隔離等級來避免,這樣就看不到還沒 commit 的改動了。

Non-Repeatable Read 的問題

想像一下,交易 1 先讀資料,另一個交易把資料改掉,然後交易 1 再讀一次,結果就不一樣了。

交易 1:

BEGIN;
SELECT balance FROM accounts WHERE owner = 'Bob'; -- 看到餘額:500。

交易 2 同時進行:

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Bob';
COMMIT;

交易 1 再來一次:

SELECT balance FROM accounts WHERE owner = 'Bob'; -- 看到餘額:400。
COMMIT;

你看,同一個交易裡資料竟然變了。在現實情境下,像財務報表這種場合就很嚴重。這個問題可以用更高的隔離等級來解決,比如 REPEATABLE READ。

Phantom Read 的問題

假設我們有一個 orders 資料表:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer TEXT NOT NULL,
    total NUMERIC(10, 2) NOT NULL
);
INSERT INTO orders (customer, total) VALUES ('Alice', 100), ('Bob', 200);

交易 1 算訂單數,另一個交易加了一筆新訂單,然後交易 1 再算一次。

交易 1:

BEGIN;
SELECT COUNT(*) FROM orders; -- 看到:2。

交易 2 同時進行:

BEGIN;
INSERT INTO orders (customer, total) VALUES ('Charlie', 300);
COMMIT;

交易 1 再來一次:

SELECT COUNT(*) FROM orders; -- 看到:3。新的「幽靈」訂單出現了!
COMMIT;

要解決幽靈讀的問題,需要用 SERIALIZABLE 隔離等級,這樣會完全鎖住會影響結果的並行改動。

避免異常的方法

隔離等級 vs. 異常狀況

隔離等級 Dirty Read Non-Repeatable Read Phantom Read
Read Uncommitted ❌ 會發生 ❌ 會發生 ❌ 會發生
Read Committed ✅ 不會 ❌ 會發生 ❌ 會發生
Repeatable Read ✅ 不會 ✅ 不會 ❌ 會發生
Serializable ✅ 不會 ✅ 不會 ✅ 不會

怎麼選對隔離等級?

  1. 如果你要的是快取資料,異常沒那麼重要(比如分析舊資料)——用 READ COMMITTED。
  2. 如果你要同一個交易裡資料都穩定——用 REPEATABLE READ。
  3. 如果你要最嚴格的隔離和一致性——選 SERIALIZABLE。記得:效能可能會變差喔。

實用建議

根據你的需求選對交易隔離等級。例如:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT ...;
COMMIT;

加上 index,可以減少 table lock,加快查詢速度。

用優化過的查詢,避免長時間鎖表,尤其是在 SERIALIZABLE 隔離等級下。

常見錯誤跟怎麼避免

有時候隔離等級選錯會造成衝突或效能下降。比如在一個有很多並行交易的系統用 SERIALIZABLE,可能會造成鎖死或交易「飢餓」。

要避免這種情況,記得分析你的查詢,在不同隔離等級下測試效能,還有用對的 index。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION