為什麼需要 Isolation

兩個事務同時執行,可能發生三種問題:

Dirty Read:事務 A 讀到事務 B 還沒 commit 的資料,B 後來 rollback,A 讀到的資料從來沒有「真正存在過」。

Non-repeatable Read:事務 A 同一個查詢執行兩次,得到不同結果——因為 B 在中間 commit 了更新。

Phantom Read:事務 A 執行範圍查詢兩次,第二次多了(或少了)row——因為 B 在中間 insert 或 delete 了符合條件的 row。

SQL 標準定義了四個 isolation level,每個允許不同種類的問題發生:

Isolation LevelDirty ReadNon-repeatable ReadPhantom Read
Read Uncommitted允許允許允許
Read Committed防止允許允許
Repeatable Read防止防止允許(標準)
Serializable防止防止防止

PostgreSQL 的 Repeatable Read 額外防止了 Phantom Read,比 SQL 標準更嚴格。MySQL InnoDB 用 gap lock 實作 Repeatable Read 防 Phantom Read。


Read Committed(PostgreSQL 預設)

每個 statement 讀到的是「這個 statement 開始時已 committed 的最新資料」。

-- 事務 A
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- 讀到 1000
-- 此時事務 B commit 了 UPDATE SET balance = 500
SELECT balance FROM accounts WHERE id = 1;  -- 讀到 500(non-repeatable read)
COMMIT;

99% 的應用用 Read Committed 就夠了。大多數「讀最新資料」的場景不需要更嚴格的隔離。


Repeatable Read

一個事務內的所有 read 都看到同一個快照(事務開始時的狀態)。

適用場景:需要在一個事務裡做多次讀取,並且要求這些讀取看到一致的狀態——例如匯出報表(不希望匯出到一半資料變了)、計算複雜的統計數字。


Serializable

最嚴格。所有並發事務的執行效果,等同於某個串行執行的順序。

適用場景:銀行轉帳、票務系統(防止超賣)、任何「A 和 B 的操作有隱性依賴」的場景。

代價:性能下降明顯,可能有更多 serialization failure(事務被 abort 要重試)。


MVCC:如何做到不用鎖就能讀

大多數現代資料庫(PostgreSQL、MySQL InnoDB)用 MVCC(Multiversion Concurrency Control)實作 isolation:每次寫操作不是覆蓋資料,而是寫一個新版本,舊版本保留。 讀操作看到符合自己 isolation level 的版本。

accounts 表(id=1):
版本 1: balance=1000, xmin=100, xmax=200  ← 被事務 200 覆蓋
版本 2: balance=500,  xmin=200, xmax=null ← 目前最新

Read Committed 讀最新的已 committed 版本;Repeatable Read 讀事務開始時的版本。

MVCC 的副產品:舊版本需要被清理——PostgreSQL 的 VACUUM 做這件事。長時間跑著的事務(沒有 commit 的 idle transaction)會阻止 VACUUM 清理舊版本,導致表膨脹。


實際使用建議

-- 查看當前 isolation level
SHOW transaction_isolation;
 
-- 設定特定事務的 isolation level
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- ...
COMMIT;
 
-- 找 idle 事務(可能阻塞 VACUUM)
SELECT pid, state, now() - xact_start AS duration
FROM pg_stat_activity
WHERE state != 'idle' AND xact_start IS NOT NULL
ORDER BY duration DESC;

大多數應用不需要手動調 isolation level——了解 Read Committed 的行為,知道什麼時候要升到 Repeatable Read 或 Serializable,就夠了。

application 層的 connection pool 設定(backend/database 章節)和 isolation level 的關係:connection pool 的連線是被多個 request 複用的,確保每個 request 開始時 transaction 是 clean state(沒有前一個 request 遺留的 begin)。