先釐清:一致性不是每一步同時成功
當訂單服務寫入資料庫後,還要通知 ERP、更新 CRM、預留庫存或發送 LINE 訊息,最常見的錯誤是把「寫入資料」與「送出訊息」當成兩個獨立動作。資料寫入成功但訊息傳送失敗,會留下無人處理的訂單;先送訊息再寫資料,則可能讓下游看見一筆最終不存在的交易。這就是典型的 dual write 問題。
跨資料庫、SaaS 與外部 API 的流程,通常不適合依賴分散式鎖定或兩階段提交。參與系統未必支援相同協定,網路也可能在結果已生效後中斷。工程上的目標因此不是假設所有步驟能形成單一原子交易,而是明確定義哪些狀態必須在本地同時成立、哪些狀態允許延遲收斂,以及失敗後由誰恢復。
Outbox 的邊界:可靠發布,不是跨系統回滾
Transactional Outbox 適合處理「本地狀態已提交,就必須可靠產生事件」的需求。應用程式在同一個資料庫交易中,同時寫入業務資料與 outbox 記錄;背景程序或 CDC 管線再將記錄發布到訊息系統。即使程序在提交後立即停止,未發布的記錄仍可被重新掃描,不會因一次網路失敗而永久遺失。
Outbox 提供的是至少一次傳遞的實務基礎,而不是自動的 exactly-once。發布端可能在訊息送出後、標記完成前當機,因此消費端仍須以事件 ID、業務鍵或版本號做冪等處理。若同一聚合的事件順序重要,還要保存 aggregate ID 與遞增版本,並在分區、重試及平行消費時維持順序。
- 適合:訂單建立後必須通知其他服務、資料異動要同步搜尋索引,或稽核事件不得因短暫故障遺失。
- 不適合:需要撤銷多個遠端系統已完成動作的長流程;Outbox 只保證事件可送達,不會決定後續如何補償。
- 必要配套:消費端去重、可控重試、死信處理、事件結構版本,以及 outbox 資料的封存或清理機制。
Saga 的邊界:協調業務交易與補償
Saga 將跨系統流程拆成多個可獨立提交的本地交易,並為後續失敗設計補償。例如企業採購流程可能依序建立訂單、保留額度、預留庫存與建立出貨要求;若出貨建立失敗,流程可以釋放庫存與額度,再把訂單標示為取消。這不是資料庫 rollback,而是另一組具有業務意義、同樣可能失敗的交易。
補償是否可行,是採用 Saga 前最重要的判斷。退款不等於刪除付款,已寄出的通知無法收回,已被人工處理的 ERP 單據也未必能自動沖銷。遇到不可逆步驟,應將它延後到必要條件都滿足後才執行,或把流程停在「待人工處理」狀態,而不是承諾不存在的全自動回復。
簡單且參與者少的流程可用事件編舞,但事件鏈一長,流程狀態、逾時與責任歸屬會變得難以追蹤。需要分支、期限、人工介入或多種補償策略時,通常應採用明確的 orchestrator,把 Saga 建模為持久化狀態機;協調器可以重啟,但流程進度不能只存在記憶體中。
實務上常是 Outbox 加 Saga
兩者處理不同層次。Saga 決定下一個業務步驟及失敗時的補償;Outbox 確保每個參與服務在提交本地狀態後,可靠地發出步驟完成事件。典型做法是:訂單服務以單一交易寫入訂單與 outbox;協調器收到事件後要求庫存服務預留;庫存服務再以自己的本地交易更新庫存並寫入 outbox。每一段都可重試,整體流程則由 Saga 狀態機推進。
設計時應從業務不變條件反推,而不是先選框架。如果要求是「本地資料與事件不可分離」,優先考慮 Outbox;如果要求是「多個系統的成功與失敗必須形成可管理的業務結果」,需要 Saga;若兩者都存在,就組合使用。若流程其實可以接受排程對帳或人工核准,較簡單的狀態同步方案可能比完整 Saga 更可靠,也更容易營運。
上線前的工程檢查
一致性設計最後會成為營運問題。每個命令與事件都應帶有全域追蹤 ID、業務 ID 與冪等鍵;流程狀態要能回答目前卡在哪一步、最後一次錯誤是什麼、下一次重試何時發生。技術錯誤如逾時可自動重試,業務拒絕如額度不足則應走明確的失敗或補償路徑,兩者不能使用同一套無限重試策略。
- 以故障注入測試提交前、提交後、發布後與消費中的中斷情境。
- 確認每個補償命令可重複執行,且不會造成二次退款、重複釋放或錯誤覆寫。
- 提供重播、人工修復與對帳工具,但限制操作者只能從已驗證的狀態轉移。
- 監控 outbox 積壓、事件處理延遲、重試次數、死信與長時間未完成的 Saga。
- 在事件契約中採向後相容的演進方式,避免部署順序成為隱藏的分散式交易。
成熟的跨系統一致性方案,不是追求表面上的即時同步,而是讓每個中間狀態都可辨識、可重試、可補償且可稽核。當 ERP、CRM、LINE 與雲端服務的限制不同,整合團隊的價值就在於把這些限制轉化為清楚的交易邊界與可操作的復原流程。