當多個微服務必須協作完成一個業務流程,怎麼避免狀態不一致?怎麼處理失敗回退?本章拆解:
- 為什麼分散式交易(distributed transactions)通常不是答案
- Saga 模式如何在沒有全域鎖的前提下協調多步驟流程
交易(Transactions)回顧#
交易把多個動作視為單一單位,要嘛全成功、要嘛全失敗。
ACID 四性質#
ACID = Atomicity、Consistency、Isolation、Durability
- 原子性(Atomicity):所有操作要嘛全部完成,要嘛全部不發生
- 一致性(Consistency):交易結束後資料庫處於有效狀態
- 隔離性(Isolation):多個交易並行時互不干擾
- 持久性(Durability):完成的交易不會因系統故障消失
關聯式資料庫普遍提供 ACID;MongoDB 4.0 之前只支援單一文件範圍的 ACID。

Figure 5.1:單一 ACID 交易內同時更新兩張表
微服務仍可使用 ACID,但範圍變小#
- 單一微服務內部的資料庫操作仍可享有 ACID
- 但跨服務操作就無法保證原子性
把原本一個 ACID 交易拆成多個服務各自的子交易後,整個流程的原子性就消失了。中途某一步失敗時,前面已 commit 的步驟不會自動回退。

Figure 5.2:Invoice 與 Order 在兩個獨立交易中變更
兩階段提交(2PC):為什麼不該用#
2PC 嘗試用兩階段協調分散式交易:
- 投票階段(Voting Phase):協調者問所有 worker 是否能執行各自的變更;任何一個說 NO 就整體中止
- 提交階段(Commit Phase):所有 worker 都同意後,協調者通知大家正式執行

Figure 5.3:2PC 投票階段,worker 表決能否執行本地變更

Figure 5.4:2PC 提交階段,變更實際套用
問題#
- 隱性鎖:worker 投票同意後,必須鎖住資源確保稍後仍能 commit;管理本機鎖已經夠難了,更何況跨節點
- 不再具備 isolation:commit 訊息到達各 worker 的時間不同,外界可能短暫看到部分節點已變更、部分尚未變更
- 延遲與失敗模式:worker 投票後沒回應 commit,怎麼辦?多種失敗模式必須人工介入
- 規模放大會放大問題
Pat Helland:「在多數分散式交易系統中,單一節點失敗會讓整個交易卡住。系統越大,越容易整體掛掉。如同要求飛機所有引擎全運作的設計——多裝一具引擎反而降低可靠性。」
結論:避開分散式交易。除非交易期極短、參與者極少。
不拆會怎樣?#
最簡單的選項是:不要把這份資料拆開。
- 若某些狀態必須以原子、一致的方式管理,就把它與相關功能一起留在單一服務(或單體)裡
- 還在拆解單體的早期階段,跨交易的部分先不動,等其他地方拆完累積經驗再回頭處理
Saga:跨服務工作流的正解#
起源#
Saga 概念來自 Hector Garcia-Molina 與 Kenneth Salem 1987 年論文,原本要解決的是長活交易(Long-Lived Transactions, LLT)——一個交易跨越數分鐘、數小時甚至數天,期間鎖住的資源會嚴重影響其他作業。
論文建議:把 LLT 拆成多個短交易,每個短交易自身具備 ACID,但整體 saga 不再追求單一原子性。
Saga 與微服務#
把業務流程拆成多步驟,每步交給不同微服務處理。每一步在自己的資料庫內可享 ACID,但 saga 整體無法 rollback。
Saga 不提供 ACID 的原子性,它提供「足以推理當前狀態」的資訊,讓你決定接下來怎麼處置。
MusicCorp 訂單履行範例#
下單 → 收款 → 給點數 → 倉庫保留庫存 → 包裝 → 出貨每步呼叫對應的微服務,內部各自 ACID。

Figure 5.5:以 saga 模型化的訂單履行流程範例
Saga 的失敗模式#
兩種復原策略:
- 向後復原(Backward Recovery):撤銷已完成步驟,需定義補償動作(compensating action)
- 向前復原(Forward Recovery):從失敗處重試,需保留足夠資訊以續跑
兩者可混用,依該步驟的本質決定。

Figure 5.6:嘗試包裝商品時,倉庫卻找不到該商品
Saga Rollback:補償交易(Compensating Transaction)#
ACID 的 rollback 是「在 commit 前取消」,但 saga 中早已 commit 的步驟需要新的補償交易來抵銷。
補償交易是語意性回退(Semantic Rollback),無法把世界恢復到原狀。
例:如果你已經寄了「訂單出貨中」的 email,補償交易能做的不是「取消那封 email」,而是「再寄一封說明訂單已取消」。
回退不代表資料消失——你可能還是希望保留這筆失敗訂單的紀錄供分析。

Figure 5.7:透過補償交易觸發整個 saga 的 rollback
重新排序步驟以減少回退#
把容易失敗的步驟提前、容易補償的步驟在前;不容易補償的步驟放後面。
例:把「給點數」從「收款後」移到「真正出貨後」——這樣若包裝失敗就不必回退點數。

Figure 5.8:將步驟移到 saga 後段以減少失敗時需 rollback 的範圍
同時混用向前與向後復原#
訂單流程後段(已收款且包裝完)若出貨失敗,整單回退反而怪——較合理的做法是重試出貨,或交由人工介入。
實作 Saga 的兩種風格#
1. 編排式(Orchestrated Saga)#
中央協調者(Orchestrator)控制整個流程。
- 指揮控制式(command-and-control)
- 大量使用請求/回應呼叫
- 業務流程集中、可見、易於理解
優點:
- 業務流程在一處明確展現,新人易上手
- 整體進度與狀態追蹤容易
缺點:
- 領域耦合提升:協調者必須認識所有下游服務
- 邏輯容易被吸進協調者,下游服務變得「貧血」(anemic)
「邏輯只要有地方可以集中,就一定會被集中。」(If logic has a place where it can be centralized, it will become centralized!)
緩解方式:用多個小型協調者各自管自己的流程(Order Processor、Returns、Goods Receiving 各管一段),都共用 Warehouse 服務。

Figure 5.9:以編排式 saga 實作訂單履行流程的範例
BPM 工具(Business Process Modeling)通常打著「讓非開發者畫流程」的旗號,但實務上幾乎都是開發者在用,且帶來版本控制差、難測試等問題。
想嘗試的話,作者建議看 Camunda、Zeebe——它們較貼近開發者工作流。
2. 編舞式(Choreographed Saga)#
責任分散到所有參與服務,靠事件驅動協作。
- 信任但驗證(trust-but-verify)
- 服務各自監聽自己關心的事件,做完後再發出事件
- 沒有中央協調者

Figure 5.10:以編舞式 saga 實作訂單履行流程的範例
優點:
- 領域耦合最低:每個服務只需知道「收到 X 事件要做 Y」
- 不會發生邏輯集中現象
缺點:
- 追蹤狀態困難:流程不在任何單一處顯式呈現
- 想知道某個 saga 走到哪一步、哪些補償該執行,沒有明顯入口
解法:為每個 saga 配一個關聯 ID(Correlation ID),跟著所有事件流動。再做一個專門的服務 vacuum 所有事件,就能投影出每筆 saga 的當前狀態。
Correlation ID 在編舞式 saga 中幾乎是必備。
混合風格#
兩種風格不互斥。例如外層採用編舞式,到了 Warehouse 內部處理「揀貨 → 包裝 → 派送」的子流程時改採編排式。
不論選哪種風格,都要有一致的方式讓你能追蹤一筆 saga 走到哪、哪裡卡住。否則錯誤排查與復原會變得非常痛苦。
編舞式 vs 編排式:怎麼選?#
| 情境 | 推薦風格 |
|---|---|
| 單一團隊負責整個 saga | 編排式(管理較簡單) |
| 多團隊共同參與 saga | 編舞式(解耦讓各團隊獨立工作) |
作者個人偏好編舞式——額外的追蹤複雜度通常被鬆耦合的好處抵銷。
Saga vs 分散式交易#
- 分散式交易(如 2PC)將失敗風險疊加:規模放大反而降低可用性
- Saga 顯式建模業務流程:流程本身成為一等公民,新進開發者更容易理解
推薦延伸閱讀:
- 第一版《Building Microservices》第 4 章
- 《Enterprise Integration Patterns》(雖未直接提 saga,但深入討論編排與編舞)
- Pat Helland《Life Beyond Distributed Transactions》(acmqueue)
小結#
- 分散式交易(特別是 2PC)在大多數場景下不該採用
- 跨服務的業務流程應顯式建模為 saga
- Saga 不提供 ACID 原子性,但提供「足以推理狀態並決定如何收拾」的資訊
- 補償交易是語意性回退,不是時光倒流
- 編排式 = 集中、可見、耦合稍高;編舞式 = 分散、鬆耦合、追蹤難
- Correlation ID 是編舞式 saga 的命脈
- 業務流程值得作為一等公民被認真設計