CodeGym /課程 /SQL SELF /REPEATABLE READ 隔離級別:防止 Non-Repeatable Read

REPEATABLE READ 隔離級別:防止 Non-Repeatable Read

SQL SELF
等級 40, 課堂 1
開放

想像一下,你在玩線上遊戲,突然有個外掛仔亂改遊戲程式碼,把自己的角色 buff 爆,或者你在圖書館看書,結果有人趁你不注意偷偷換掉書頁、插入新章節,甚至直接換掉整本書。這種情況是不是超級煩?REPEATABLE READ 隔離級別就是要幫你擋掉這種「驚喜」。

REPEATABLE READ 保證你在同一個交易裡看到的資料,從頭到尾都不會變。就算有別的交易想改這些資料,你的交易也不會被影響,超安心。

重點特色:

  • 防止 Dirty Read(讀到還沒 commit 的資料)。
  • 最重要的是,防止 Non-Repeatable Read。意思就是你在交易一開始查到一組資料,之後再查一次還是那一組,就算別人偷偷改了也沒差。

不過 REPEATABLE READ 沒辦法防 Phantom Read。如果有別的交易插入新資料,你再查一次可能會看到新資料。要完全防這種情況就要用 SERIALIZABLE,這個我們之後再聊。

怎麼設定 REPEATABLE READ 隔離級別

在看範例之前,先來搞懂怎麼在 PostgreSQL 開啟這個隔離級別。有兩種主要方式:

  1. 只設定這次交易的隔離級別:

    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    BEGIN;
    -- 你的查詢
    COMMIT;
    
  2. 設定目前 session 的隔離級別:

    SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    

第二種方式的話,這個 session 裡所有交易都會用 REPEATABLE READ。

範例:防止 Non-Repeatable Read

假設我們有一張 accounts 表,結構長這樣:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_name TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending'
);

INSERT INTO orders (customer_name, status)
VALUES ('Alice', 'pending'), ('Bob', 'pending');

先來看最基本的情境,一個交易改資料,另一個交易讀資料。

沒有 REPEATABLE READ 的情境(READ COMMITTED 級別)

交易 1 開始:

BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- 取得:100

這時候交易 2 改資料:

BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;

交易 1 繼續:

SELECT balance FROM accounts WHERE account_id = 1;
-- 取得:150(資料變了!)
COMMIT;

你看,READ COMMITTED 隔離級別下,同一個交易裡兩次查詢資料可能會變,這就是 Non-Repeatable Read 啦。

有 REPEATABLE READ 的情境

現在我們用一樣的例子,但隔離級別設成 REPEATABLE READ。

交易 1:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- 取得:100

交易 2:

BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;

交易 1 繼續:

SELECT balance FROM accounts WHERE account_id = 1;
-- 還是取得:100(資料沒變!)
COMMIT;

不管別的交易怎麼改,交易 1 看到的資料都跟一開始一樣。這樣 Non-Repeatable Read 就被擋下來了。

REPEATABLE READ 怎麼運作

PostgreSQL 用 MVCC(Multi-Version Concurrency Control)來實作 REPEATABLE READ 隔離級別。MVCC 的核心就是每個交易都拿到一個穩定的「快照」,這個快照在交易結束前都不會變。這是靠建立和管理多個 row 版本來做到的。

當交易開始時,它看到的資料就是那一刻的狀態。別的交易如果有改動,PostgreSQL 會產生新的 row 版本,但舊的版本還是留給需要它的交易用。

所以交易會比較慢,也會吃比較多記憶體。這也是為什麼很少人會直接用最強的隔離級別:雖然超穩,但效能會掉很多。

REPEATABLE READ 的限制:Phantom Read

剛剛有提到,REPEATABLE READ 沒辦法防 Phantom Read。來看一個查詢範圍資料的例子。

假設我們有一張 orders 表:

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    amount NUMERIC NOT NULL
);

INSERT INTO orders (amount)
VALUES (50), (100), (150);

交易 1:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- 取得:2

交易 2:

BEGIN;
INSERT INTO orders (amount) VALUES (200);
COMMIT;

交易 1 繼續:

SELECT COUNT(*) FROM orders WHERE amount > 50;
-- 取得:3(新 row 出現了!)
COMMIT;

這裡有一個新 row(amount = 200)被別的交易加進來,結果交易 1 查詢時就「幽靈」般出現了,雖然用的是 REPEATABLE READ。

如果你真的要防 Phantom Read,就只能用 SERIALIZABLE,但這樣效能一定會掉,得自己權衡。

REPEATABLE READ 的優缺點

REPEATABLE READ 隔離級別超適合你想確保資料在交易期間不會變的時候用。你查到什麼,commit 前都不會變,就算別人想改也沒用。

這種做法可以防 dirty read 跟 non-repeatable read。你一直用的都是一開始那份資料,不會突然被偷偷改掉。這對做報表或需要資料一致性的決策超有用。

但 REPEATABLE READ 擋不住「幽靈 row」(phantom read)——就是你查過的資料範圍,突然多了新 row。還有,如果交易很多、大家都查同一批資料,可能會有鎖定衝突,甚至被 rollback,就算 SQL 沒寫錯也會這樣。

總之,REPEATABLE READ 算是穩定跟效能的好平衡,但如果競爭很激烈,還是要多注意設定跟調校。

實用小技巧跟常見錯誤

  • 記得,隔離級別會影響效能。只有真的需要資料不變時才用 REPEATABLE READ。
  • 常常有人搞混 REPEATABLE READ 跟 SERIALIZABLE。如果你查詢時看到新 row,這在 REPEATABLE READ 是正常的。
  • 長交易要小心鎖定衝突。交易拖太久,可能會卡住別人的操作。

PostgreSQL 給你很多工具來控管交易隔離。REPEATABLE READ 很適合你想確保同一個交易裡資料不會變的時候用。

2
任務
SQL SELF, 等級 40, 課堂 1
上鎖
防止 Non-Repeatable Read(不可重複讀)在 `REPEATABLE READ` 隔離級別下發生
防止 Non-Repeatable Read(不可重複讀)在 `REPEATABLE READ` 隔離級別下發生
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION