假設你的銀行帳戶有 1,000 元,你要轉 300 元給朋友。從人的角度看,這是一件事:轉帳。但對資料庫而言,它至少可能包含兩個動作:
- 從你的帳戶扣掉 300 元。
- 在朋友的帳戶增加 300 元。
問題來了:如果第一步成功後,伺服器突然斷電,第二步還沒執行,會發生什麼?
你的 300 元消失了,但朋友沒有收到。
這就是 Transaction(交易、事務)要解決的核心問題:當多個資料庫操作在邏輯上屬於同一件事時,系統不應該允許它停在只完成一部分的狀態。
Transaction:把多個操作包成一件事
Transaction 可以把一連串資料庫操作視為一個工作單位。最典型的流程像這樣:
BEGIN;
UPDATE accounts SET balance = balance - 300 WHERE id = 'A';
UPDATE accounts SET balance = balance + 300 WHERE id = 'B';
COMMIT;
BEGIN 表示交易開始,COMMIT 則表示這一整組修改確認完成。
如果中途出錯,程式可以執行 ROLLBACK,把這次 Transaction 尚未確認的變更撤回。於是結果不再是「A 扣了錢、B 沒收到」,而是要嘛兩邊都成功,要嘛整筆交易失敗並回到原本狀態。
這個概念不只存在於銀行。電商下單可能要建立訂單、扣庫存、建立付款紀錄;學校系統可能要建立選課紀錄並更新剩餘名額;遊戲交易可能要從玩家 A 移除物品,再加入玩家 B 的背包。只要多個資料修改必須共同成立,就可能需要 Transaction。
ACID:Transaction 常見的四個核心性質
談 Transaction 時,通常會遇到 ACID。它不是某個資料庫產品名稱,而是四個性質的縮寫:Atomicity、Consistency、Isolation、Durability。
### Atomicity:要嘛全部成功,要嘛全部失敗
Atomicity 是原子性。
「原子」在這裡可以理解為不可再拆開的邏輯單位。轉帳雖然包含兩次 UPDATE,但對系統而言應該被當成一件完整操作。
如果第二個 UPDATE 失敗,第一個 UPDATE 也不能單獨留下。
因此 Atomicity 關心的是:一組操作會不會只完成一半?
### Consistency:資料不能被帶到不合法的狀態
Consistency 是一致性。
假設系統規定庫存不能小於 0、某欄位必須唯一,或帳戶必須符合特定資料規則。Transaction 執行前後,資料應維持系統定義的合法條件。
但這裡很容易誤解:資料庫的 Consistency 並不代表資料庫會自動理解所有商業邏輯。
例如「每位學生最多選 30 學分」如果只存在程式規則裡,資料庫不一定自己知道。開發者仍需要透過 constraint、資料模型與應用程式邏輯共同維持一致性。
### Isolation:同時發生的交易不要互相踩壞
真正麻煩的地方通常從並行開始。
假設商品只剩最後 1 件,兩位使用者幾乎同時下單:
使用者 A 查詢庫存:1。
使用者 B 查詢庫存:1。
A 判斷可以購買。
B 也判斷可以購買。
A 扣掉庫存。
B 也扣掉庫存。
如果系統沒有正確處理並行,最後可能賣出兩件根本不存在的商品。
Isolation(隔離性)處理的就是不同 Transaction 同時執行時,彼此應該看見多少尚未完成的資料,以及資料庫如何避免互相干擾。
### Durability:COMMIT 之後不能說忘就忘
Durability 是持久性。
當資料庫告訴應用程式「COMMIT 成功」,這筆資料就應該被可靠保存。即使接著服務重新啟動或機器發生故障,已確認的交易也不應莫名消失。
資料庫通常會利用 Write-Ahead Log(WAL)、redo log 等機制,先留下足以恢復資料的紀錄,再完成後續儲存工作。
因此 Durability 關心的不是「操作有沒有執行」,而是「已經宣告成功的資料,故障後還在不在」。
COMMIT 和 ROLLBACK 到底差在哪?
可以把一個 Transaction 想成在草稿區修改資料。
COMMIT 是確認:這些修改正式成立。
ROLLBACK 是取消:這次尚未確認的修改不要了。
例如:
BEGIN;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 42;
INSERT INTO orders (...) VALUES (...);
如果 INSERT 因為資料錯誤失敗,應用程式可以 ROLLBACK。這樣前面的庫存修改也不會孤零零留下。
但要注意,Transaction 不是「所有外部世界都能倒帶」。如果程式在 Transaction 中間已經寄出 Email、呼叫第三方 API 或讓物流系統建立出貨單,資料庫的 ROLLBACK 通常不能把那些外部動作一起撤銷。
這也是為什麼大型系統會出現 Outbox Pattern、Saga 等更進一步的設計:當一件事跨越多個服務或外部系統時,單一資料庫 Transaction 已經不夠。
為什麼有 Transaction 還會出現並行 Bug?
因為「有使用 Transaction」不代表所有 Transaction 都像排隊一樣完全依序執行。
如果資料庫強迫每一筆交易都等上一筆完全結束,資料雖然比較容易保持安全,效能卻可能非常差。因此現代資料庫通常允許大量 Transaction 並行,再透過鎖、MVCC(Multi-Version Concurrency Control)與 Isolation Level 控制它們互相看見的資料。
這就產生幾種經典問題。
### Dirty Read
Transaction A 修改一筆資料,但還沒有 COMMIT。Transaction B 卻先讀到了這個尚未確認的值。
如果 A 最後 ROLLBACK,B 剛才看到的其實是一個從未正式存在過的資料狀態。
### Non-repeatable Read
同一個 Transaction 裡,第一次讀某筆資料得到 100,過一下再讀卻變成 120,因為中間另一個 Transaction 已經修改並 COMMIT。
同一筆資料在同一次工作流程中讀到不同結果,就可能讓程式邏輯變得難以推理。
### Phantom Read
Transaction 第一次查詢「所有金額大於 1,000 的訂單」得到 10 筆。另一個 Transaction 新增一筆符合條件的訂單並 COMMIT 後,原本的 Transaction 再查一次,變成 11 筆。
不是原本某一列被修改,而是符合查詢條件的資料列集合發生變化,像突然多出一個「幻影」。
Isolation Level:安全和效能之間的選擇
SQL 資料庫常見的隔離層級包括:
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
一般來說,隔離越強,Transaction 越接近「彷彿只有自己在操作資料庫」,但資料庫可能需要更多等待、衝突處理或重試,吞吐量也可能受到影響。
SERIALIZABLE 通常提供最接近序列執行的語意,但它不是免費的效能按鈕。實際系統需要依資料庫實作、資料競爭程度與正確性需求選擇。
而且不同資料庫對同名 Isolation Level 的細節可能不同。因此真正開發時,不能只背四個名稱,還要查看使用中的 PostgreSQL、MySQL、SQLite 或其他資料庫實際提供什麼行為。
Transaction 也可能失敗:Deadlock
假設 Transaction A 先鎖住資料 X,再等待 Y;Transaction B 則先鎖住 Y,再等待 X。
A 等 B,B 又等 A,兩邊都無法繼續,這就是 Deadlock(死結)。
資料庫通常會偵測這種狀況,主動終止其中一個 Transaction,讓另一個繼續。因此應用程式不能假設 Transaction 只要開始就一定成功;遇到 deadlock、serialization failure 等情況時,常常需要安全地 retry。
這也帶出另一個重要觀念:Transaction 應盡量保持短小。
如果程式 BEGIN 後等待使用者操作、呼叫一個很慢的外部 API,再過幾秒才 COMMIT,鎖或資料版本可能被維持很久,增加其他請求等待與衝突的機率。
Transaction 不是「加上去就安全」
常見錯誤是先 SELECT,再在應用程式裡判斷,最後 UPDATE,卻沒有考慮兩個請求可能同時完成 SELECT。
例如:
SELECT stock FROM inventory WHERE id = 42;
程式看到 stock = 1,判斷可以購買,接著才執行:
UPDATE inventory SET stock = stock - 1 WHERE id = 42;
兩個請求可能都在 UPDATE 前看到 1。
更可靠的做法可能是把條件直接放進原子更新:
UPDATE inventory
SET stock = stock - 1
WHERE id = 42 AND stock > 0;
再檢查實際更新了幾列。另一種情況則可能需要 SELECT ... FOR UPDATE、適當的 isolation level 或資料庫 constraint。
重點不是背哪一種寫法,而是辨認真正需要保護的 invariant:系統有哪些條件無論多少請求同時進來,都不能被破壞?
Transaction 和 API Request 不是同一件事
一個 HTTP request 可以包含一個 Transaction,也可能包含很多個 Transaction;一個 Transaction 也不必知道 HTTP 的存在。
例如建立訂單的 API:
POST /orders
後端收到 request 後,可能開啟 Transaction,檢查與扣除庫存、建立 order、建立 order_items,最後 COMMIT,再回傳 HTTP 201。
HTTP 是網路通訊層面的 request/response;Transaction 則是資料一致性層面的工作單位。兩者經常一起出現,但不是同一個概念。
什麼時候應該想到 Transaction?
當你發現一句需求裡出現「同時」、「必須一起」、「不能只成功一半」時,就值得檢查 Transaction。
例如:
建立訂單,同時扣除庫存。
把學生加入班級,同時增加班級人數。
建立留言,同時更新文章留言統計。
從一個帳戶扣款,同時替另一個帳戶入帳。
但也不要把整個應用程式都塞進一個超大的 Transaction。Transaction 範圍越大,持有鎖與資源的時間通常越長,也更容易產生 contention。
理想的 Transaction boundary 應該包住真正需要共同成功的資料修改,而且盡可能短。
最後整理
Database Transaction 的核心不是 SQL 裡多了 BEGIN 和 COMMIT,而是替資料定義「這些操作在邏輯上是一件事」。
Atomicity 避免只成功一半;Consistency 要求資料維持合法規則;Isolation 處理多個交易同時執行時的互相干擾;Durability 則確保已經 COMMIT 的結果能在故障後保留下來。
真正困難的地方通常也不是單一使用者按下按鈕,而是十個、千個甚至更多請求同時修改同一批資料時,系統仍能維持那些不能被破壞的規則。
所以當你下一次看到「扣庫存」「轉帳」「搶最後一張票」這類需求時,可以先問一個比「SQL 要怎麼寫」更重要的問題:如果流程執行到一半失敗,或者兩個人同時執行,資料還會是對的嗎?
如果答案不確定,Transaction 就是你接下來應該理解的地方。
把概念放回真正的系統流程裡看,通常比只背名詞更容易理解它。