在單體架構中,資料庫的本地 ACID 事務(Local Transaction)透過寫入鎖與 WAL 日誌,讓「跨表更新」具備完美的原子性(Atomicity)與隔離性(Isolation)。

然而,當系統拆分為微服務後,原本單一的資料庫操作演化為跨越「訂單服務」、「庫存服務」、「支付服務」與「物流服務」的多個獨立跨網路 RPC/訊息呼叫。此時,傳統的本地事務徹底失效。

若採用強一致性的兩階段提交(2PC),嚴苛的全局鎖定會讓微服務的高吞吐量與可用性崩潰。為此,Saga 模式(Saga Pattern) 成為了處理分散式長事務(Long-Running Distributed Transactions)的工業標準。

本文將從 Saga 的基本原理出發,深度剖析 協同式(Choreography) 與 編排式(Orchestration) 兩大架構形態、補償事務設計原則,以及如何應對 Saga 缺乏隔離性帶來的業務風險。


1. Saga 模式基本原理:本地事務鏈與補償機制

Saga 模式將一個大型的分散式全域事務拆分為一系列有序的本地事務(Local Transactions)序列:T_1, T_2, ..., T_n。

  • 每個服務在執行本地事務時,僅鎖定本服務的本地資料庫,執行完成立即提交事務釋放鎖。
  • 正向執行成功:當 T_1 -> T_2 -> ... -> T_n 全部順利完成,全域 Saga 宣告成功。
  • 局部步驟失敗與補償:若第 k 步本地事務 T_k 執行失敗(例如庫存不足或扣款失敗),Saga 必須依相反順序觸發一組補償事務(Compensating Transactions) C_{k-1}, ..., C_1,將先前已提交的歷史變更在業務語義上「撤銷/沖正」,達到最終一致性。
Saga 分散式長事務:正向執行鏈與失敗補償鏈 展示 Saga 正向事務 T1 到 T4 依序提交成功,以及當 T3 扣款失敗時依相反順序觸發 C2 釋放庫存與 C1 取消訂單的反向補償達成最終一致性。FORWARD EXECUTION PIPELINE✅ 正向成功路徑(本地事務鏈依序提交釋放鎖)T1: 建立訂單訂單服務 (Commit)T2: 預扣庫存庫存服務 (Commit)T3: 執行扣款支付服務 (Commit)T4: 物流出貨物流服務 (成功完成)特點:每個服務僅鎖定自身資料庫,執行完畢即刻 Commit,高吞吐無全局排他鎖阻塞。COMPENSATING ROLLBACK PIPELINE❌ 失敗回滾路徑(T3 扣款失敗 ➔ 逆序觸發業務沖正補償 C2, C1)T1: 建立訂單C1: 標記訂單取消T2: 預扣庫存C2: 釋放預扣庫存T3: 扣款失敗!✗卡片餘額不足 / 銀行逾時最終一致性無懸掛孤立資料鐵律:補償不是底層 ROLLBACK,而是全新補償交易(須具備冪等性、重試不允許失敗、可補償步驟置於樞紐之前)。
FORWARD SUCCESS

✅ 正向成功鏈 (Forward Path)

1. T1 (訂單):建立訂單並提交。

2. T2 (庫存):預扣庫存並提交。

3. T3 (支付):扣款成功並提交。

4. T4 (物流):生成運單並成功完成。

VS. 遇到業務例外時
COMPENSATING ROLLBACK

❌ 失敗回滾鏈 (Compensation Path)

1. T3 失敗:支付帳戶餘額不足,中止正向流程。

2. C2 補償:呼叫庫存服務釋放已凍結之庫存(沖正)。

3. C1 補償:呼叫訂單服務更新狀態為已取消。

補償交易須遵循冪等性與重試必定成功原則,達成全鏈路最終一致性。

圖一:Saga 模式正向本地事務推進與失敗逆向補償架構對比

2. 兩大架構拓撲:協同式 (Choreography) vs. 編排式 (Orchestration)

評估維度Choreography (事件協同式)Orchestration (集中編排式)
控制中心去中心化,完全依賴事件廣播集中式 Saga 協調器 (Orchestrator)
服務間偶合度極低,服務只需監聽對應領域事件服務需依賴協調器的統一呼叫排程
業務流程可視化困難 (散落在各個 Consumer 代碼中)極佳 (由狀態機集中定義執行圖)
循環依賴風險高 (容易形成複雜網狀事件迴路)零 (單向調度拓撲)
適用複雜度簡單流程 (2~4 個服務節點)複雜商業鏈條 (5 個以上跨服務長事務)

2.1 協同式(Choreography):去中心化的事件發布/訂閱

服務之間沒有中央協調者,各服務透過發布與訂閱領域事件(Domain Events via Kafka/RabbitMQ)自主推動下一步:

  1. 訂單服務建立訂單,發布 OrderCreated 事件。
  2. 庫存服務監聽 OrderCreated,扣減庫存後發布 InventoryReserved 事件。
  3. 支付服務監聽 InventoryReserved,執行扣款。若扣款失敗,發布 PaymentFailed 事件。
  4. 庫存服務與訂單服務各自監聽 PaymentFailed 事件,分別執行補償回滾。

2.2 編排式(Orchestration):基於狀態機的中央調度器

由專屬的 Saga Orchestrator(協調器,如 Temporal、AWS Step Functions、Cadence) 集中管理全域事務狀態機:

  1. 協調器向訂單服務發送「建立訂單」指令,等待回執。
  2. 協調器向庫存服務發送「扣減庫存」指令,等待回執。
  3. 若某一步拋出異常,協調器根據狀態機定義,依序主動發起「釋放庫存」、「取消訂單」等補償指令。

3. 補償事務的四大工程鐵律

補償事務並不是簡單的 ROLLBACK,而是透過一條全新的 UPDATE 或 INSERT 進行業務沖正。在設計補償動作時,必須嚴格遵守以下四條原則:

  1. 冪等性(Idempotence):由於網路抖動或重試,補償事務可能被重複呼叫多次。每次補償操作必須依賴唯一的 SagaId + StepId 保證重複執行結果完全一致。
  2. 絕對不允許失敗(Cannot Fail):補償事務一旦觸發,必須透過死循環重試、本地持久佇列或死信佇列(DLQ)保證最終必定執行成功;若遇致命系統故障,需報警交由人工介入對帳。
  3. 不可逆操作的順序後置(Pivot Step):
    • 可補償事務(Compensatable Transactions):能夠被撤銷的操作(如預扣庫存、凍結資金)。
    • 樞紐事務(Pivot Transaction):一旦成功執行即不可逆的操作(如真實向第三方銀行發起信用卡扣款)。
    • 可重試事務(Retriable Transactions):在樞紐事務之後執行的操作,保證必定會成功(如發送確認 Email、列印出貨單)。
    • 設計原則:樞紐事務必須放在所有可補償事務之後執行!
  4. 防懸掛與空補償處理:若正向請求因網路延遲尚未到達服務端,而協調器已因逾時提前發送了補償請求,服務端必須記錄該 Saga 的取消狀態,當遲到的正向請求抵達時直接拒絕執行。

4. Saga 的致命短板:缺乏隔離性 (ACID 中的 I)

Saga 滿足了 ACD(原子性、一致性、持久性),但完全放棄了 ACID 中的 I(隔離性)。因為在整個 Saga 尚未結束前,中間步驟產生的本地資料變更已經被 COMMIT 到了資料庫中,對外完全可見!

這會引發嚴重的併發異常:

  • 髒讀(Dirty Read):Saga A 扣減了庫存,此時 Saga B 讀取到剩餘庫存為 0 並拒絕了其他客戶;隨後 Saga A 支付失敗回滾了庫存。
  • 更新丟失(Lost Update):Saga A 將庫存從 10 改為 8;在 Saga A 尚未結束時,Saga B 讀取到 8 並將其改為 5;若此時 Saga A 發生失敗補償,將庫存加回 2 改為 7,導致 Saga B 的扣減被覆蓋丟失。

4.1 解決隔離性缺失的防禦對策

  1. 語義鎖(Semantic Lock / Soft State):
    • 不要直接扣減庫存,而是將商品狀態標記為 PENDING_RESERVED。
    • 任何其他交易看到 PENDING 狀態時,必須等待或拒絕操作,直到 Saga 結束轉為 CONFIRMED。
  2. 悲觀防禦(Pessimistic View):
    • 讀取資料時假設所有進行中的 Saga 都會成功,計算可用配額時主動扣除 Pending 總量。
  3. 操作可交換性(Commutative Updates):
    • 將扣減操作設計為可交換的數學算式(如 UPDATE account SET balance = balance - 100),避免覆蓋絕對值。

5. 架構選型總結

  • 流程簡單、服務個數少於 3 個:選用 Choreography(協同式),透過 Kafka 事件串聯,開發成本最低。
  • 核心金融、訂單履約、長達數小時的審批流:強烈推薦引入 Orchestration(編排式)(如 Temporal),透過程式碼定義狀態機,具備全鏈路可觀測性、精確重試與超時補償能力。