拆成微服務那天,你同時失去了一樣東西。
在單體架構裡,「建立訂單、扣庫存、收付款」三件事可以塞進同一個 database transaction——要嘛全成功,要嘛全失敗,沒有中間狀態。ACID 保證讓你睡得著覺。
微服務拆開之後,三個服務、三個資料庫,那個 transaction 無法跨越服務邊界。你需要一種新的方式協調跨服務的資料一致性。Saga Pattern 就是目前最主流的答案——但它解決問題的方式,是把問題轉移到另一個地方。
為什麼不是 2PC
2PC(Two-Phase Commit)是最直覺的解法:讓一個協調者問所有服務「你準備好了嗎?」,全員回答「可以」之後再一起 commit。理論上完美。
實務上,2PC 把可用性和效能同時犧牲了。Prepare 到 Commit 之間,所有參與服務的資源被鎖住。如果協調者在這個時間點掛了,所有服務卡在 Prepare 狀態,不知道要 commit 還是放棄,需要人工介入。更根本的問題是:2PC 要求所有服務同時在線,整個鏈的可用性是各服務可用性的乘積——三個 99.9% 的服務組在一起,2PC 事務的可用性只剩 99.7%,而且網路延遲會疊加,每一個參與者都是效能瓶頸。
Saga 選擇了不同的取捨:放棄強一致性(strong consistency),換取可用性和效能。
Saga 的本質:補償而非回滾
Saga 把一個跨服務的業務流程拆成一連串的 local transaction,每個 local transaction 只動自己服務的資料庫。每個步驟都必須有對應的「補償操作」(compensating transaction):
正常流程:
T1(建訂單)→ T2(扣庫存)→ T3(收付款)→ T4(確認訂單)
T3 失敗時——不是回滾 T1 和 T2,它們已經 commit 了:
C2(恢復庫存)→ C1(取消訂單)
這個區別很重要。傳統 transaction 的失敗是「這件事沒有發生」,Saga 的失敗是「這件事發生了,但我們把它的效果消除掉」。兩者從用戶角度看起來類似,但在系統內部,Saga 要求每個業務步驟天生支援這種「反向操作」——在設計業務流程時,就必須同時設計失敗路徑。
Choreography vs Orchestration:這個選擇比你想的重要
Saga 有兩種協調方式,但這不只是技術選擇,是架構風格的選擇,影響的是整個服務群的可觀測性和耦合方式。
Choreography(編舞):沒有中央協調者。每個服務完成工作後發布事件,其他服務訂閱事件並自行決定要做什麼。整個 Saga 的流程隱式地分散在各個事件處理器裡:
訂單服務 ──publish──→ [order.created]
庫存服務 ←─subscribe─┘ → 扣庫存 ──publish──→ [stock.reserved]
付款服務 ←─subscribe─┘ → 收付款 ──publish──→ [payment.completed]
訂單服務 ←─subscribe─┘ → 確認訂單
Orchestration(編排):有一個 Saga Orchestrator 明確定義每個步驟的順序,呼叫各服務,等待回應,決定下一步。流程集中在一個地方:
Saga Orchestrator:
Step 1 → 呼叫訂單服務(建訂單)
Step 2 → 呼叫庫存服務(扣庫存)
Step 3 → 呼叫付款服務(收付款)
Step 4 → 呼叫訂單服務(確認)
失敗時 Orchestrator 負責反向執行補償。
選擇 Choreography,你得到真正的服務解耦——訂單服務不知道庫存服務的存在,只是發布了 order.created。代價是可見性消失了。Saga 失敗時,你要追蹤散落在多個服務日誌裡的 event 鏈才能知道發生了什麼。新加一個步驟,要修改多個服務並確保事件訂閱關係沒有循環依賴。
選擇 Orchestration,可見性找回來了。Orchestrator 是流程的唯一真相來源,整個 Saga 的狀態集中在一個地方,debug 直接。代價是 Orchestrator 知道所有服務的存在,成了一個間接的耦合中心,本身也需要高可用設計。
判斷框架:流程超過 3 個步驟、可能動態增加步驟、或者需要清楚的 audit trail,選 Orchestration。Choreography 在兩個服務之間的簡單流程很優雅,但規模一大,「沒有人知道完整流程在哪」會成為維運噩夢。大部分團隊用 Orchestration 更能控制長期複雜度。
在 Pattern 圖譜裡,Saga 在哪裡
Saga 常常跟 Event Sourcing 和 CQRS 一起出現,但它們解決的是不同層次的問題,可以獨立使用。
Saga 解決跨服務協調問題——怎麼讓多個服務的資料在業務邏輯層面保持一致。
Event Sourcing 解決狀態儲存問題——用事件序列取代狀態快照。Saga Orchestration 跟 Event Sourcing 很搭:Orchestrator 持久化每個步驟的 event,重啟後可以從事件日誌恢復到失敗前的狀態繼續執行,不怕 Orchestrator 本身的中途崩潰。
CQRS 解決讀寫模型衝突——在 Saga 語境裡,CQRS 常用在「Saga 執行期間用戶查詢狀態」的場景:訂單在 Saga 執行中是 PENDING,用戶端用獨立的讀模型查看,不必等 Saga 全部完成才能顯示任何資訊。
三者可以組合,也可以單獨使用。Saga 不要求 Event Sourcing,CQRS 也不要求有 Saga。
什麼時候你不需要 Saga
Saga 引入的複雜度是真實的,在引入之前值得先確認問題真的存在。
服務共用同一個資料庫——這種情況直接用 DB transaction 就好,Saga 解決的問題根本不存在。微服務的形式不代表資料層必須分離。
跨服務操作可以接受 eventual consistency,且沒有補償需求——如果「發送通知」失敗了不需要回滾任何事情,Message Queue 加 retry 就夠了,不需要 Saga 的完整補償機制。
業務流程只有兩個步驟且補償操作是冪等的——Choreography 一個事件就搞定,引入 Orchestrator 是過度工程。
Saga 真正需要的場景:多個服務、多個 DB、業務要求「失敗必須回到一致狀態」、且補償操作非同步執行是可接受的。
Saga 引入的真實代價
Saga 讓你解決了分散式 transaction 問題,但它把問題轉移了——從「怎麼保持一致性」變成了「怎麼正確處理每一種失敗路徑」。
補償操作本身可能失敗。補償失敗了要 retry;retry 次數耗盡還是失敗,要有 Dead Letter Queue;DLQ 裡的紀錄要有人處理,要有告警,要有 runbook。微服務的錯誤處理邏輯通常比業務邏輯更複雜——這不是 Saga 的設計缺陷,而是分散式系統本身的現實,Saga 只是讓這個現實變得可以管理。
在引入 Saga 之前,值得和團隊確認:你們有能力設計、測試、維護所有補償路徑嗎?補償失敗的 alert 和 runbook 是誰負責?這些問題沒有答案之前,Saga 不會讓系統更穩,只會讓它更難理解。
實作細節和程式碼範例 → 46:Saga Pattern 取代分散式 Transaction
下一篇 → CQRS:讀寫模型分離