想像一下,你在玩線上遊戲,突然有個外掛仔亂改遊戲程式碼,把自己的角色 buff 爆,或者你在圖書館看書,結果有人趁你不注意偷偷換掉書頁、插入新章節,甚至直接換掉整本書。這種情況是不是超級煩?REPEATABLE READ 隔離級別就是要幫你擋掉這種「驚喜」。
REPEATABLE READ 保證你在同一個交易裡看到的資料,從頭到尾都不會變。就算有別的交易想改這些資料,你的交易也不會被影響,超安心。
重點特色:
- 防止
Dirty Read(讀到還沒 commit 的資料)。 - 最重要的是,防止
Non-Repeatable Read。意思就是你在交易一開始查到一組資料,之後再查一次還是那一組,就算別人偷偷改了也沒差。
不過 REPEATABLE READ 沒辦法防 Phantom Read。如果有別的交易插入新資料,你再查一次可能會看到新資料。要完全防這種情況就要用 SERIALIZABLE,這個我們之後再聊。
怎麼設定 REPEATABLE READ 隔離級別
在看範例之前,先來搞懂怎麼在 PostgreSQL 開啟這個隔離級別。有兩種主要方式:
只設定這次交易的隔離級別:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- 你的查詢 COMMIT;設定目前 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 很適合你想確保同一個交易裡資料不會變的時候用。
GO TO FULL VERSION