本文 AI 產出,尚未審核

MySQL 交易 (Transaction) – 隔離級別 (Isolation Level)


簡介

在資料庫系統中,交易 (Transaction) 是確保資料一致性與完整性的核心機制。當多個使用者同時對同一筆資料進行讀寫時,若沒有適當的控制,就會產生「髒讀、不可重複讀、幻讀」等問題。MySQL 透過 隔離級別 (Isolation Level) 來定義不同交易之間的可見性與衝突處理方式,讓開發者可以在效能與資料正確性之間取得平衡。

了解與正確設定 Isolation Level,不僅能避免資料錯誤,還能提升系統的併發效能,是每位 MySQL 開發者必備的基礎知識。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握 MySQL 的四大隔離級別。


核心概念

1. 什麼是 Isolation Level?

Isolation Level 定義了 交易在執行期間看到的其他交易修改 的可見性。SQL 標準規範了四種主要的隔離級別:

隔離級別 可能發生的問題 說明
READ UNCOMMITTED 髒讀 (Dirty Read) 交易可以讀取尚未提交的資料。
READ COMMITTED 不可重複讀 (Non‑Repeatable Read) 只能讀取已提交的資料,但同一筆資料在同一交易內可能被其他交易改寫。
REPEATABLE READ (MySQL 預設) 幻讀 (Phantom Read) 同一筆資料在交易期間保持一致,但新插入的列仍可能出現。
SERIALIZABLE 完全隔離,等同於所有交易排隊執行,效能最低。

注意:MySQL InnoDB 引擎的預設隔離級別是 REPEATABLE READ,它透過多版本併發控制 (MVCC) 防止髒讀與不可重複讀,僅在極少數情況下才會產生幻讀。

2. 四大隔離級別的行為差異

2.1 READ UNCOMMITTED

  • 特性:允許讀取其他交易尚未提交的變更。
  • 適用情境:報表或統計分析,對即時性要求高且容忍少量不一致的情況。

2.2 READ COMMITTED

  • 特性:每次 SELECT 都只能看到已提交的資料。
  • 適用情境:大多數 OLTP 系統的預設選擇,兼顧一致性與效能。

2.3 REPEATABLE READ

  • 特性:在同一交易內的多次 SELECT 結果一致,即使其他交易已提交修改。
  • 適用情境:需要保證同筆資料在交易期間不變的業務,如訂單處理、庫存扣減。

2.4 SERIALIZABLE

  • 特性:所有交易以排隊方式執行,等同於一次只允許一筆交易存取資料。
  • 適用情境:極端需要絕對一致性的批次處理或財務結算。

3. 如何設定 Isolation Level?

MySQL 提供兩種設定方式:

  1. 全域層級(影響所有新連線)
    SET GLOBAL transaction_isolation = 'REPEATABLE-READ';
    
  2. 會話層級(僅影響當前連線)
    SET SESSION transaction_isolation = 'READ-COMMITTED';
    

Tip:在 MySQL 8.0 之後,transaction_isolation 已被 transaction_isolation 替代為 transaction_isolation,但兩者仍可相容使用。


程式碼範例

以下示範四個隔離級別在實務中的差異。所有範例均使用 InnoDB 引擎的 accounts 表,欄位說明如下:

CREATE TABLE accounts (
    id      INT PRIMARY KEY,
    name    VARCHAR(50),
    balance DECIMAL(10,2)
) ENGINE=InnoDB;

INSERT INTO accounts VALUES (1, 'Alice', 1000.00), (2, 'Bob', 800.00);

範例 1:READ UNCOMMITTED – 髒讀

-- 連線 A (Transaction 1)
SET SESSION transaction_isolation = 'READ UNCOMMITTED';
START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;  -- Alice 扣款,但尚未 commit

-- 連線 B (Transaction 2) 同時執行
SET SESSION transaction_isolation = 'READ UNCOMMITTED';
SELECT balance FROM accounts WHERE id = 1;  -- 會看到 800.00,雖然 Transaction 1 尚未 commit

-- 若 Transaction 1 rollback,B 讀到的 800.00 就是「髒資料」。
ROLLBACK;   -- 交易 1 回滾

重點:在 READ UNCOMMITTED 下,任何未提交的變更都能被其他交易讀到,極易產生資料不一致。

範例 2:READ COMMITTED – 不可重複讀

-- 連線 A
SET SESSION transaction_isolation = 'READ COMMITTED';
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 2;   -- 讀到 800.00

-- 連線 B
SET SESSION transaction_isolation = 'READ COMMITTED';
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- Bob 加款
COMMIT;

-- 回到連線 A,重新執行相同查詢
SELECT balance FROM accounts WHERE id = 2;   -- 現在讀到 900.00,結果變了
COMMIT;

解說:同一筆資料在同一交易內被讀了兩次,結果不同,這就是 不可重複讀

範例 3:REPEATABLE READ – 防止不可重複讀

-- 連線 A
SET SESSION transaction_isolation = 'REPEATABLE READ';
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;   -- 讀到 1000.00

-- 連線 B
SET SESSION transaction_isolation = 'READ COMMITTED';
START TRANSACTION;
UPDATE accounts SET balance = balance - 300 WHERE id = 1;  -- Alice 扣款
COMMIT;

-- 連線 A 再次查詢相同資料
SELECT balance FROM accounts WHERE id = 1;   -- 仍然是 1000.00,因為 MVCC 提供快照
COMMIT;

要點:即使其他交易已提交修改,REPEATABLE READ 仍會返回交易開始時的快照,確保「同一筆資料在同一交易內不可變」。

範例 4:SERIALIZABLE – 防止幻讀

-- 連線 A
SET SESSION transaction_isolation = 'SERIALIZABLE';
START TRANSACTION;
SELECT COUNT(*) AS cnt FROM accounts WHERE balance > 500;  -- 假設返回 2

-- 連線 B (同時執行)
SET SESSION transaction_isolation = 'SERIALIZABLE';
START TRANSACTION;
INSERT INTO accounts VALUES (3, 'Charlie', 600.00);  -- 新增一筆符合條件的資料
COMMIT;   -- 會因為鎖定衝突而被阻塞,直到 A 完成

-- 連線 A 完成
COMMIT;   -- 此時 B 才能繼續執行

說明:在 SERIALIZABLE 下,MySQL 會在執行 SELECT ... WHERE 時自動加上範圍鎖 (range lock),防止其他交易插入符合條件的新列,從而避免 幻讀

範例 5:動態切換 Isolation Level

有時候同一個應用程式在不同的業務流程需要不同的隔離級別,可以在程式碼層面動態切換:

const mysql = require('mysql2/promise');

async function transfer(conn, fromId, toId, amount) {
    // 針對轉帳流程使用 REPEATABLE READ,確保金額一致性
    await conn.query("SET SESSION transaction_isolation = 'REPEATABLE READ'");
    await conn.beginTransaction();

    try {
        await conn.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', [amount, fromId]);
        await conn.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', [amount, toId]);

        await conn.commit();
    } catch (err) {
        await conn.rollback();
        throw err;
    }
}

實務提示不要在同一次交易中混用不同的 Isolation Level,這會導致 MySQL 產生不可預期的鎖定行為。


常見陷阱與最佳實踐

陷阱 說明 解決方案
誤用 READ UNCOMMITTED 在需要精確金額計算的金融系統仍使用髒讀,可能導致錯帳。 除非是純讀取性報表,否則應避免使用此級別。
忘記在程式碼中顯式設定 Isolation Level 依賴資料庫全域預設,導致不同環境行為不一致。 在應用程式啟動時或每筆交易開始前明確 SET SESSION
過度使用 SERIALIZABLE 產生大量鎖爭用,系統效能急遽下降。 僅在確實需要「絕對一致」的批次作業使用。
忽略 InnoDB 的自動鎖定 以為只要設定 Isolation Level 就能防止所有衝突,實際上仍需考慮 行鎖間隙鎖 了解 MySQL 的鎖定機制,必要時使用 SELECT ... FOR UPDATE 明確加鎖。
在長交易中使用高隔離級別 交易持續時間過長會佔用大量快照資源,引發 MVCC 版本膨脹 將交易拆分成較小的粒度,或在非關鍵路徑使用較低隔離級別。

建議的最佳實踐

  1. 預設使用 REPEATABLE READ:MySQL 已針對大多數 OLTP 場景進行最佳化。
  2. 對於報表或分析查詢使用 READ COMMITTED,以降低鎖定衝突。
  3. 使用 SELECT ... FOR UPDATELOCK IN SHARE MODE 明確控制行鎖,配合適當的 Isolation Level。
  4. 在程式碼層面統一管理 Transaction:使用框架的 Transaction Manager,避免手動遺漏 COMMIT/ROLLBACK
  5. 監控鎖爭用:透過 SHOW ENGINE INNODB STATUSperformance_schema 或 APM 工具,及時發現高併發下的鎖等待。

實際應用場景

業務需求 推薦 Isolation Level 為什麼
線上訂單扣庫存 REPEATABLE READ + SELECT ... FOR UPDATE 防止同時扣除同一筆庫存,確保庫存不會變負。
即時金融交易 REPEATABLE READ(或 SERIALIZABLE) 必須保證金額計算的原子性與一致性。
每日營收報表 READ COMMITTED 只需要已提交的資料,對暫時不一致容忍度高。
批次資料匯入 SERIALIZABLE(或暫時提升至 SERIALIZABLE) 確保匯入期間不會產生幻讀或重複資料。
多租戶 SaaS 系統的租戶分表查詢 READ COMMITTED + 行鎖 減少跨租戶的鎖爭用,提高併發度。

案例說明:假設一個電商平台在「下單」流程中,同時要檢查庫存、扣減庫存、產生訂單。若使用 READ COMMITTED,在高併發下可能發生兩筆交易同時看到相同的庫存數量,導致超賣。改為 REPEATABLE READ 並加上 SELECT ... FOR UPDATE,即可在庫存行上加鎖,保證同一時間只有一筆交易能修改該筆庫存。


總結

  • Isolation Level 是交易的核心概念,決定了交易之間資料可見性的範圍。
  • MySQL 提供四種標準隔離級別,從 READ UNCOMMITTED(最鬆)到 SERIALIZABLE(最嚴)。
  • REPEATABLE READ 為 InnoDB 的預設值,利用 MVCC 提供高效能且避免髒讀與不可重複讀。
  • 正確選擇與設定 Isolation Level 必須結合業務需求、效能考量與鎖定機制。
  • 實務上,明確在程式碼中設定 Session Isolation Level使用行鎖避免長時間高隔離級別交易,是保證系統穩定與效能的關鍵。

掌握了上述概念與實作技巧,你就能在 MySQL 中靈活控制交易的行為,既保證資料正確性,又不犧牲系統的併發效能。祝你在開發過程中玩得開心、寫出高品質的資料庫程式碼!