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 提供兩種設定方式:
- 全域層級(影響所有新連線)
SET GLOBAL transaction_isolation = 'REPEATABLE-READ'; - 會話層級(僅影響當前連線)
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 版本膨脹。 | 將交易拆分成較小的粒度,或在非關鍵路徑使用較低隔離級別。 |
建議的最佳實踐
- 預設使用 REPEATABLE READ:MySQL 已針對大多數 OLTP 場景進行最佳化。
- 對於報表或分析查詢使用 READ COMMITTED,以降低鎖定衝突。
- 使用
SELECT ... FOR UPDATE或LOCK IN SHARE MODE明確控制行鎖,配合適當的 Isolation Level。 - 在程式碼層面統一管理 Transaction:使用框架的 Transaction Manager,避免手動遺漏
COMMIT/ROLLBACK。 - 監控鎖爭用:透過
SHOW ENGINE INNODB STATUS、performance_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 中靈活控制交易的行為,既保證資料正確性,又不犧牲系統的併發效能。祝你在開發過程中玩得開心、寫出高品質的資料庫程式碼!