前言

工程師們,大家午安!今天想跟大家聊一個看似老生常談,但其實超級重要、而且常常被誤解的概念:資料庫交易(Transaction)隔離等級(Isolation Level)

欸,不要看到「交易」兩個字就想說跟錢有關就頭痛,它跟我們每天寫的程式、處理的資料息息相關。想想看,你平常在開發的時候,有沒有遇過這樣的狀況:

  • 客戶回報說,他們的訂單狀態明明已經成功,但庫存卻沒有扣到?
  • 或者使用者抱怨,他剛買完票,但重新整理頁面後,票又可以買了?
  • 更嚴重一點,你轉帳給朋友,結果你的錢扣了,朋友卻沒收到?

這些問題的背後,常常都跟「資料庫交易」沒搞清楚,或者「隔離等級」設定不對有關係。今天,我們就來用最白話的方式,把這些概念拆解開來,讓你知道為什麼不能只做一半,以及怎麼在效能跟資料一致性之間做取捨。

什麼是「交易 (Transaction)」?為什麼你轉帳不能只做一半?

想像一下你去銀行轉帳,整個流程通常是這樣:

  1. 從你的帳戶扣錢
  2. 把錢加到對方的帳戶

這兩個步驟必須「同時成功」或「同時失敗」,不能只完成其中一個。如果你的錢扣了,對方卻沒收到,那麻煩就大了。這個「要嘛全成功、要嘛全失敗」的原子操作集合,就是 **交易 (Transaction)**。

在資料庫領域,我們通常用 ACID 這四個字來描述一個交易應該具備的特性:

1. 原子性 (Atomicity):要嘛全成功,要嘛全失敗

這就是我前面說的「轉帳」例子。所有步驟綁在一起,像一個不可分割的原子。如果交易中的任何一個操作失敗,整個交易就會被取消(Rollback),所有變更都會回復到交易開始前的狀態。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 開始一個交易
BEGIN TRANSACTION;

-- 從帳戶 A 扣 100 元
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 'A';

-- 如果這裡發生錯誤,例如帳戶 A 餘額不足,
-- 或者網路斷線等等,整個交易都應該被取消。

-- 將 100 元加到帳戶 B
UPDATE Accounts SET Balance = Balance + 100 WHERE AccountID = 'B';

-- 如果兩個操作都成功,就提交交易
COMMIT;

-- 如果中間有任何一步失敗,就取消交易
-- ROLLBACK;

2. 一致性 (Consistency):交易前後資料必須維持合法狀態

資料庫從一個合法的狀態開始,經過一個交易後,會到達另一個合法的狀態。舉例來說,如果規定帳戶餘額不能是負數,那轉帳交易完成後,所有帳戶餘額都必須是非負數。這個特性通常是透過資料庫的約束(Constraint)、觸發器(Trigger)和程式碼邏輯來保證的。

3. 隔離性 (Isolation):交易之間互不影響

多個交易同時執行時,它們的執行結果應該與它們「依序執行」的結果相同。白話來說,就是一個交易在進行的時候,不應該看到其他還沒完成的交易對資料做了什麼修改。這點非常重要,也是我們今天文章的重點「隔離等級」在處理的問題。

4. 持久性 (Durability):交易提交後,資料永不消失

一旦交易成功提交(Commit),即使系統發生故障(例如斷電),資料庫中的變更也應該永久保存下來。這通常是透過將資料寫入永久儲存裝置(硬碟)來實現的。

為什麼需要「隔離等級」?多個人同時操作資料,問題就來了!

想像一下,一個資料庫同時有成千上萬個使用者在操作,都在讀取、寫入、更新資料。如果沒有適當的隔離機制,這些同時執行的交易就會互相干擾,導致資料錯誤。這就像多個人同時編輯一份文件,如果沒有鎖定機制,就會亂成一團。

資料庫系統為了處理這種併發(Concurrency)問題,提供了不同的 隔離等級(Isolation Level)。不同的隔離等級,就是用來定義一個交易在執行時,能夠「看到」多少來自其他併發交易的資料變更。隔離等級越高,資料的一致性越好,但通常效能會越差;反之,隔離等級越低,效能越好,但資料出錯的風險越高。

常見的併發問題有三種,它們會在不同的隔離等級下被解決:

1. 髒讀 (Dirty Read / Uncommitted Read)

一個交易讀取到另一個還沒提交的交易修改過的資料。如果那個未提交的交易後來取消了(Rollback),那麼第一個交易讀到的資料就是「髒」的、根本不存在的。

情境: 小明在 ATM 轉帳 1000 元給小華,但還沒按確定(未提交)。此時小華去查詢餘額,發現多了 1000 元。結果小明發現轉錯了,取消交易。小華再查,錢又不見了!

2. 不可重複讀 (Non-Repeatable Read)

一個交易在同一個事務中,多次讀取同一筆資料,結果前後不一致。這是因為在第一次讀取之後、第二次讀取之前,另一個已提交的交易修改了這筆資料。

情境: 小華查詢自己的帳戶餘額是 5000 元。此時小明成功轉帳 1000 元給小華(已提交)。小華再次查詢餘額,變成 6000 元。同一筆交易中,兩次讀取結果不同。

3. 幻影讀 (Phantom Read)

一個交易在同一個事務中,多次查詢符合相同條件的「一組」資料,結果前後不一致。這是因為在第一次查詢之後、第二次查詢之前,另一個已提交的交易新增或刪除了符合條件的資料。

情境: 小華查詢所有餘額大於 5000 元的帳戶列表,看到 5 個。此時小明新開了一個餘額 10000 元的帳戶(已提交)。小華再次查詢所有餘額大於 5000 元的帳戶列表,看到 6 個。同一筆交易中,查詢到的「資料數量」不同。

四種隔離等級,從最寬鬆到最嚴格

不同的資料庫系統(例如 MySQL、PostgreSQL、SQL Server)雖然實作上會有些微差異,但普遍都支持標準 SQL 定義的四種隔離等級:

1. Read Uncommitted (讀取未提交資料) - 最低隔離

  • 允許的問題: 髒讀、不可重複讀、幻影讀。
  • 特性: 一個交易可以看到其他未提交交易所做的修改。這提供了最高的併發性,但資料一致性風險也最高。通常只在對資料正確性要求極低,且追求極致速度的場景下使用。

實務場景: 幾乎不用!非常危險。

2. Read Committed (讀取已提交資料) - 避免髒讀

  • 允許的問題: 不可重複讀、幻影讀。
  • 特性: 一個交易只能看到其他已提交交易所做的修改。這是很多資料庫(例如 PostgreSQL、SQL Server、Oracle)的預設隔離等級。
  • 解決: 髒讀。

實務場景: 大部分網路應用程式的預設選擇,在效能和資料一致性之間取得不錯的平衡。

3. Repeatable Read (可重複讀) - 避免不可重複讀

  • 允許的問題: 幻影讀。
  • 特性: 一個交易在讀取資料時,會對所有讀取到的資料加上共享鎖(Shared Lock),確保在該交易結束前,這筆資料不會被其他交易修改。這保證了在同一個交易中,多次讀取同一筆資料的結果都是一致的。
  • 解決: 髒讀、不可重複讀。
  • 注意: MySQL 的 Repeatable Read 透過多版本併發控制 (MVCC) 機制,其實也能避免大部分的幻影讀問題。這點是 MySQL 和標準 SQL 定義的差異,也是一個常考的知識點。

實務場景: 需要確保在特定交易中資料讀取一致性高的報表生成、複雜的業務邏輯處理等。MySQL 的預設值。

4. Serializable (可序列化) - 最高隔離

  • 允許的問題: 無。
  • 特性: 這是最高的隔離等級,保證所有交易都是序列化執行(彷彿一個接一個執行),完全避免了髒讀、不可重複讀和幻影讀。它會對讀取和寫入的資料都加鎖,強制交易串行執行。
  • 解決: 所有併發問題。
  • 代價: 併發性最低,效能最差。因為會加很多鎖,很容易造成死鎖(Deadlock)。

實務場景: 對資料一致性有極端要求,且併發量不高的關鍵業務。但因為效能犧牲太大,極少作為系統預設值。

實務上怎麼選?

隔離等級的選擇,是效能與資料一致性之間的權衡。

  • 大部分應用 (網頁、API): Read Committed 是一個很常見且合理的選擇。它解決了最危險的髒讀問題,同時保有不錯的併發性。對於不可重複讀和幻影讀,通常會在應用程式層面用其他方式處理(例如:重新讀取最新資料、使用樂觀鎖/悲觀鎖)。

  • MySQL 應用: 由於 MySQL 預設是 Repeatable Read,而且它對幻影讀的處理方式也相對完善,所以很多 MySQL 開發者會直接用預設值。但如果你不熟悉 MVCC 機制,仍需注意潛在的併發問題。

  • 需要嚴格一致性: 如果你的業務邏輯真的非常複雜,對資料一致性要求極高,甚至每個查詢都必須保證資料不變,那可能才會考慮 Repeatable ReadSerializable。但這時候,你一定要仔細評估其帶來的效能衝擊和鎖競爭問題。

  • 設定方式: 大部分資料庫允許你設定全域的隔離等級,也可以在單一會話(Session)或單一交易中臨時設定:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    -- 設定全域隔離等級 (需要重啟資料庫服務才會生效)
    SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

    -- 設定當前會話的隔離等級
    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

    -- 在單一交易中設定隔離等級 (通常放在 BEGIN TRANSACTION 之前)
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION;
    -- 你的 SQL 語句
    COMMIT;

小結

資料庫交易與隔離等級,是每個後端工程師都應該透徹理解的核心概念。它們不是抽象的理論,而是實實在在影響你系統穩定性、資料正確性、甚至使用者體驗的基石。

理解 ACID 特性,以及四種隔離等級分別解決了哪些併發問題,可以幫助你在設計系統架構、撰寫業務邏輯,甚至除錯的時候,能更精準地找出問題點,並做出正確的技術決策。下次遇到資料不一致的問題,別忘了回來看看這篇文章,也許就能找到答案!

希望這次的白話拆解,能讓大家對資料庫交易有更清楚的認識。有任何問題或想法,都歡迎在下面留言跟我分享喔!