返回文章列表
mysql

Transaction Isolation Levels

深入解析 SQL 標準定義的四種交易隔離等級,以及 InnoDB 在各等級下的實際行為與實作機制

Aaron

交易的四個特性(ACID)

特性意義由誰保證
Atomicity(原子性)交易要嘛全做,要嘛全不做Undo Log
Consistency(一致性)交易前後資料滿足所有約束應用層 + A、I、D 共同保證
Isolation(隔離性)併發交易互不干擾MVCC + Locks
Durability(持久性)COMMIT 後資料不遺失Redo Log

Consistency 不是某個單一機制能保證的,它是其他三者正確運作加上應用邏輯正確的結果。

三種讀異常

SQL 標準定義了三種併發讀異常,隔離等級就是「你願意接受哪幾種」:

Dirty Read(髒讀)

讀到其他交易尚未 COMMIT 的資料。如果對方 ROLLBACK,你讀到的就是不存在的幽靈資料。

T1: UPDATE accounts SET balance = 0 WHERE id = 1;  -- 未 COMMIT
T2: SELECT balance FROM accounts WHERE id = 1;      -- 讀到 0(髒讀)
T1: ROLLBACK;                                        -- balance 其實還是原來的值

Non-Repeatable Read(不可重複讀)

同一筆交易內兩次讀同一行,結果不同——因為中間有其他交易 COMMIT 了修改。

T1: SELECT balance FROM accounts WHERE id = 1;  -- 讀到 1000
T2: UPDATE accounts SET balance = 500 WHERE id = 1; COMMIT;
T1: SELECT balance FROM accounts WHERE id = 1;  -- 讀到 500(不可重複讀)

Phantom Read(幻讀)

同一筆交易內兩次用相同條件查範圍,結果筆數不同——因為中間有其他交易 INSERT 或 DELETE 了符合條件的行。

T1: SELECT * FROM orders WHERE amount > 100;     -- 3 筆
T2: INSERT INTO orders (amount) VALUES (200); COMMIT;
T1: SELECT * FROM orders WHERE amount > 100;     -- 4 筆(幻讀)

注意:幻讀針對的是行數變化(INSERT/DELETE),不可重複讀針對的是同一行值變化(UPDATE)。

四種隔離等級

SQL 標準從低到高定義了四種隔離等級:

隔離等級Dirty ReadNon-Repeatable ReadPhantom ReadInnoDB 實作
Read Uncommitted○ 可能○ 可能○ 可能不用 MVCC,直接讀最新值
Read Committed× 防止○ 可能○ 可能每次 SELECT 建新 Read View
Repeatable Read× 防止× 防止△ 部分防止交易首次 SELECT 建 Read View,之後重用
Serializable× 防止× 防止× 防止所有 SELECT 加 S lock

InnoDB 預設是 Repeatable Read(RR),而且在 RR 下透過 MVCC + Next-Key Lock 把幻讀也擋掉了大部分,這是比 SQL 標準更強的保證。

InnoDB 在各等級下的行為

Read Uncommitted

幾乎不用。直接讀 Buffer Pool 中的最新版本,不走 MVCC,所以能讀到別人未 COMMIT 的修改。唯一優勢是零額外開銷,但實務上沒人願意承擔髒讀的風險。

Read Committed(RC)

每次 SELECT 都建立一個新的 Read View(快照),所以能看到「截至這次 SELECT 為止所有已 COMMIT 的修改」。

特性:

  • 不會髒讀:Read View 過濾掉未 COMMIT 的交易
  • 會不可重複讀:兩次 SELECT 的 Read View 不同,中間有 COMMIT 就看得到
  • 會幻讀:同理,新 INSERT 被 COMMIT 後下次 SELECT 看得到
  • 鎖行為:只鎖符合條件的行(record lock),不加 gap lock,因此鎖衝突少

很多網際網路公司(阿里巴巴內部規範)選 RC 而非 RR,原因是 RC 的 gap lock 少、死鎖概率低、併發表現好,代價是需要在應用層處理不可重複讀。

Repeatable Read(RR)— InnoDB 預設

交易中第一次 SELECT 建立 Read View,之後所有 SELECT 重用同一份。所以不管別的交易怎麼改,你在同一筆交易裡看到的快照不會變。

-- Session A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 讀到 1000,建立 Read View

-- Session B
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;

-- Session A(仍在同一筆交易)
SELECT balance FROM accounts WHERE id = 1;  -- 仍然讀到 1000(Read View 沒變)
COMMIT;

RR 下的幻讀防護

InnoDB 在 RR 下用兩種手段防幻讀:

  1. 快照讀(Snapshot Read):一般 SELECT 走 MVCC Read View,天生不受其他交易 INSERT 影響。
  2. 當前讀(Current Read)SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETE 讀的是最新版本,InnoDB 用 Next-Key Lock(record lock + gap lock)鎖住掃描過的範圍,阻止其他交易在這個範圍內 INSERT。

但有一個著名的邊界情況:

-- Session A(RR)
START TRANSACTION;
SELECT * FROM orders WHERE id = 5;  -- 不存在,回傳空(快照讀,沒加鎖)

-- Session B
INSERT INTO orders (id, amount) VALUES (5, 200);
COMMIT;

-- Session A
UPDATE orders SET amount = 300 WHERE id = 5;  -- 成功!UPDATE 是當前讀,看到了 Session B 的 INSERT
SELECT * FROM orders WHERE id = 5;            -- 讀到 id=5, amount=300(幻讀!)

快照讀說不存在,但 UPDATE 當前讀發現存在,修改完後快照讀也看到了——因為自己改過的行會進入自己的版本鏈。要完全防止這種情況,第一次查詢就要用 SELECT ... FOR UPDATE 觸發當前讀 + gap lock。

Serializable

所有 SELECT 自動加 LOCK IN SHARE MODE(S lock),等價於所有讀都是當前讀 + 鎖。完全消除幻讀,但併發度極低,幾乎等於序列化執行。只有對一致性要求極端的場景才會用(分散式交易的 2PC 內部)。

RC vs RR:怎麼選

面向RCRR
Read View每次 SELECT 新建首次 SELECT 建立,之後重用
Gap Lock有(Next-Key Lock)
死鎖概率較高(gap lock 交叉容易死鎖)
幻讀快照讀防、當前讀靠 Next-Key Lock 防
Binlog 格式要求需要 ROW(STATEMENT 不安全)STATEMENT 也安全
適用高併發 OLTP、能接受不可重複讀需要交易內一致快照

沒有絕對的好壞。RR 是安全預設;RC 是效能導向但要求開發者理解後果。

查看與設定

-- 查看當前隔離等級
SELECT @@transaction_isolation;

-- Session 級別設定
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 全域設定(新連線生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- my.cnf 永久設定
-- [mysqld]
-- transaction-isolation = READ-COMMITTED

隔離等級與 Log 的關係

保證依賴的機制
原子性(回滾)Undo Log 記錄舊值,ROLLBACK 時回復
隔離性(讀)MVCC(Read View + Undo Log 版本鏈)
隔離性(寫)Locks(Record Lock、Gap Lock、Next-Key Lock)
持久性Redo Log(WAL)+ Binlog(兩階段提交)
一致性以上所有 + 應用邏輯

詳見 InnoDB LogsMVCC