# Lesson 5: 避免資料打架-Transaction 交易處理 (ACID) 與 Race Condition

身為後端的我們，很常聽到：這裡要用交易、這不符合ACID原則…等。這篇文章主要來探討：Transition的用法與ACID的概念。

# ACID

ACID 原則是Transition時應具備的四個特性縮寫，分別是原子性（Atomicity）、一致性（Consistency）、隔離性（Isolation）和持久性（Durability）

## 原子性

**「要就全部成功，否則全部失敗」**。 如果交易中有 10 個步驟，執行到第 9 步失敗了，資料庫必須 **Rollback（回滾）** 到第 1 步還沒執行前的狀態，不能只剩下一半的資料。

> 筆記： 這裡的「原子性」跟資料庫正規化（1NF）的原子性不一樣：
> 
> * **1NF 的原子性：** 指的是「欄位不可再分割」（例如地址欄位不能塞 JSON）。
>     
> * **Transaction 的原子性：** 指的是「操作步驟不可分割」（All or Nothing，類似股票的 FOK - Fill or Kill 指令）。
>     

## **一致性**

交易執行前後，資料庫都必須維持在「合法」的狀態。 所有的資料變動都必須符合資料庫預設的約束（Constraints），例如：

* 欄位的資料型態（Type）
    
* 主鍵約束（Primary Key）
    
* 外鍵約束（Foreign Key）
    
* 檢查約束（CHECK Constraints，例如餘額不能小於 0）
    

如果交易導致資料違反了這些約束，資料庫就會報錯並回滾，確保資料永遠符合規則。

## **隔離性**

多併發的交易之間互相不會影響。 理想狀態下，交易 A 在 `COMMIT` 之前，交易 B 應該看不到交易 A 所做的修改。

* **舉例：** 同一筆資料 X，請求 A 修改了它但還沒 Commit，請求 B 此時讀取 X，應該要讀到舊的資料，而不是 A 改到一半的髒資料（Dirty Read）。
    

> 注意： 隔離性是有分級別的，級別越高越安全，但效能越差。大多數資料庫預設並非最高級別，這也是導致 Race Condition 的主因。

## **持久性**

transition commit之後的更新是永久變更的。即使資料庫立刻崩潰、斷電或重啟，這筆寫入的資料也不會消失（通常是透過 Write-Ahead Logging, WAL 機制寫入硬碟來保證）

# Race Condition

雖然 ACID 的「隔離性」聽起來很美好，但在高併發的現實世界中，如果不做額外處理，還是會發生 **Race Condition**。

**經典案例：搶票系統** 假設某演唱會門票只剩 **1 張**。

1. **使用者 A** 讀取資料庫，看到剩餘票數 = 1。
    
2. **使用者 B** 讀取資料庫，也看到剩餘票數 = 1（因為 A 還沒買完）。
    
3. **使用者 A** 執行購買：`UPDATE tickets SET count = 0`。
    
4. **使用者 B** 執行購買：`UPDATE tickets SET count = 0`。
    

**結果：** 兩個人都覺得自己買到了，但實際上票只有一張。這就是「資料打架」。

通常解決資料打架的方法就是採取Lock的方式，Lock相關概念會在L6提到。

為了解決上述提到的「Race Condition」以及平衡「資料安全」與「效能」，資料庫定義了四種隔離級別。但在了解級別之前，我們先看看如果隔離做得不好，會發生哪些靈異現象（讀取異常）。

## 讀取異常

### 髒讀 (Dirty Read)

**定義：** 讀取到別人「還沒 Commit」的髒資料。

* **情境：**
    
    1. A 修改了某筆資料（將餘額 100 改為 200），但還沒 Commit。
        
    2. B 讀取該筆資料，讀到了 **200**。
        
    3. 結果 A 因為發生錯誤 Rollback 了（餘額變回 100）。
        
    4. **後果：** B 拿著這筆不存在的「200」去做了後續操作，導致系統帳務錯誤。
        

### 不可重複讀 (Non-Repeatable Read)

**定義：** 在同一個 Transaction 內，兩次讀取同一筆資料，結果不一樣。

* **重點：** 針對的是資料的 **更新 (UPDATE)**。
    
* **情境：**
    
    1. A 讀取庫存，看到 **10** 個。
        
    2. B 執行購買並 Commit，將庫存改為 **9**。
        
    3. A 再次讀取庫存，發現變成 **9** 了。
        
    4. **後果：** A 會覺得很困惑：「我明明還在同一個交易裡，怎麼資料憑空改變了？」這會影響到後續邏輯的一致性。
        

### 幻讀 (Phantom Read)

**定義：** 在同一個 Transaction 內，兩次查詢「符合條件的筆數」，結果不一樣（像是產生幻覺）。

* **重點：** 針對的是資料的 **新增 (INSERT) 或 刪除 (DELETE)**。
    
* **情境：**
    
    1. A 查詢「所有未付款訂單」，共有 **5** 筆。
        
    2. B 此時 **新增** 了一筆未付款訂單並 Commit。
        
    3. A 再次查詢，發現變成了 **6** 筆。
        
    4. **後果：** A 明明鎖定了範圍，卻突然多出（或少了）一筆資料，這在統計報表或批次處理時會造成嚴重錯誤。
        

## 四種隔離等級

為了防止上述問題，SQL 標準定義了四種級別。級別越高，資料越安全，但效能越差（因為鎖越多）。

### Level 1: Read Uncommitted (讀取未提交)

* **特性：** 最寬鬆的級別，幾乎沒有隔離。別人還沒 Commit 的資料你也讀得到。
    
* **解決問題：** 無。
    
* **可能發生：** 髒讀、不可重複讀、幻讀。
    
* **應用場景：** 極少使用，除非完全不在乎數據正確性只求最快速度（例如某些 Log 分析）。
    
* 傳統底層行為：**幾乎不加鎖**。讀取時不加鎖，甚至無視別人寫入加的鎖。所以最快，但也最髒。
    

### Level 2: Read Committed (讀取已提交)

* **特性：** 只能讀到別人「已經 Commit」的資料。這是大多數資料庫（如 PostgreSQL, Oracle, SQL Server）的**預設值**。
    
* **解決問題：** 解決了 **髒讀**。
    
* **可能發生：** 不可重複讀、幻讀。
    
* **應用場景：** 絕大多數的一般網頁應用。
    
* 傳統底層行為：**短暫的鎖**。讀取時會加鎖，但讀完**立刻釋放**，不用等到 Transaction 結束。所以別人可以在你交易途中修改資料（導致不可重複讀）。
    

### Level 3: Repeatable Read (可重複讀)

* **特性：** 保證在同一個 Transaction 中，不管讀幾次，資料都跟第一次讀到的一樣。這是 MySQL (InnoDB) 的預設值。
    
* **解決問題：** 解決了 **髒讀**、**不可重複讀**。
    
* **可能發生：** 依 SQL 標準仍可能發生 **幻讀**。
    
    * *(補充：雖然標準說會發生幻讀，但 MySQL 的 InnoDB 引擎透過 Next-Key Lock 技術，在這個級別下其實已經能解決大部分的幻讀問題。)*
        
* 傳統底層行為：**長效的鎖**。讀取時加鎖，且一直**握住直到 Transaction 結束 (Commit/Rollback)** 才釋放。這保證了這段期間沒人能改這筆資料。
    

### Level 4: Serializable (序列化)

* **特性：** 最高級別。強制 Transaction 必須「排隊」執行（序列化），完全不允許併發。
    
* **解決問題：** 解決所有問題（髒讀、不可重複讀、幻讀）。
    
* **可能發生：** 效能極低，容易發生 Timeout 或 Deadlock。
    
* **應用場景：** 涉及金錢且完全不能容忍一絲誤差的極端場景。
    
* 傳統底層行為：**範圍鎖 (Range Lock)**。不只鎖住你讀的那一行，連「這行資料附近的範圍」都鎖住，防止別人在此範圍內 **新增 (Insert)** 資料。
    

> 現代資料庫的變革：MVCC (多版本控制)
> 
> 這裡要特別補充一個**關鍵知識點**，這也是為什麼很多人覺得「我都開了 Repeatable Read，為什麼不用等鎖？」的原因。
> 
> 現代資料庫（MySQL InnoDB, PostgreSQL, Oracle）為了效能，**在「讀取」時，通常不直接用 Lock，而是改用 MVCC (Multi-Version Concurrency Control)。**
> 
> * **這是什麼意思？** 當你在 Level 3 (Repeatable Read) 讀取資料時，資料庫不是去「鎖住」那行資料，而是給了你一張「當下的快照 (Snapshot)」。
>     
>     * **Lock 的作法：** 把門鎖起來，別人進不來。
>         
>     * **MVCC 的作法：** 給你一張照片看，別人去改原本的房間沒關係，反正你看的是照片。
>         
> 
> **所以，現代資料庫的關聯變成了：**
> 
> * **隔離級別** 決定了 **快照 (Snapshot) 的生成時機**。例如：
>     
>     * **RC 級別：** 每執行一個 SQL 語句，就重新拍一張快照（所以會看到別人剛改的）。
>         
>     * **RR 級別：** 第一次 SELECT 時拍一張快照，後面都看這一張（所以資料永遠一樣）。
>         
> * **Lock (鎖)：** 主要用於 **「寫入 (Write)」** 或 **「顯式鎖定 (Explicit Locking, 如 FOR UPDATE)」**。
>     

MVCC 雖然解決了讀取的效能與一致性問題，但因為我們看的是「照片」，所以在需要「搶票、扣款」這種**基於最新數據進行修改**的場景中，光靠 MVCC 是不夠的！我們需要手動加鎖。
