iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0

前面提到的電商註冊只是個簡單流程,它並不需要交易保證。

在單體應用中,只要以 Transaction begin/commit 或 @Transactional 就能跨表保證「全部成功或全部失敗」。發生錯誤時資料庫會自動 rollback,開發體驗單純。

但在分散式架構中,跨服務、跨資料庫、跨邊界的操作無法用單一資料庫交易涵蓋。
你會面臨最終一致性、網路失敗、重試等挑戰,這時候就需要以「補償」思維設計交易流程:
每個步驟各自完成與提交,一旦後續出錯,透過對應的反向操作把整體狀態拉回來。

1. Saga Pattern

1.1 什麼是 Saga Pattern

Saga 是一種將「大交易」拆解為多個「本地交易」的模式。每個本地交易成功後立即提交;
若後續步驟失敗,系統依序執行對應的補償交易(Compensation)來撤銷先前已完成的效果,達到最終一致性。

重點特性:

  • 本地提交:每個服務用自己的資料庫交易,成功就 commit。
  • 可補償:為每一步定義可逆的業務操作,不是資料庫 rollback,而是「業務上的反向行為」。
  • 最終一致:整體狀態經過補償後回到一致,不追求即時強一致。

1.2 編排風格:Choreography 與 Orchestration

  • Choreography(編舞):各服務透過事件彼此觸發與訂閱,沒有中央協調者。
    • 優點:鬆耦合、容易擴展小型場景。
    • 缺點:流程隱性、跨團隊治理困難、錯誤處理分散。
  • Orchestration(編排):由中央協調者(例如 Temporal Workflow)調度每一步、統一錯誤處理與補償。
    • 優點:流程可見、錯誤處理一致、測試與演進容易。
    • 缺點:需要引入協調服務與治理機制。

在需要清楚掌控流程、錯誤與補償策略一致的中大型系統,建議採 Orchestration;如同本系一開始談到 Temporal 提供這樣的編排能力。

何時適用/不適用

適用:

  • 跨 bounded contexts 的長流程業務(下單、出貨、退款、商品上架)。
  • 跨服務副作用多、需要嚴謹補場策略。
  • 可以為每一步定義「可逆」的補償行為。

不適用:

  • 單一資料庫內需要強一致的短交易(直接用資料庫交易更好)。
  • 大量「不可逆」操作且無法設計替代補償(需改造業務或採審核/人工介入)。

設計重點

  • 明確交易邊界與步驟切分:每步有清晰的輸入/輸出與成功條件。
  • 補償對應表:為每一步定義反向操作,並保證能安全重複執行。
  • 冪等性:正向與補償都要冪等,避免重試導致重複扣減或重複刪除。
  • 超時與重試策略:合理設計 Activity 的 Timeout/Retry,避免無限等待或雪崩重試。
  • 順序性與併發控制:必要時用唯一業務鍵、鎖或序列化來消除競態。
  • 可觀測性:事件歷史、關聯 ID、審計日誌,方便追查與重放。

範例:商品上架流程(T1/T2/T3)

https://ithelp.ithome.com.tw/upload/images/20250928/20141146pqOjLZrd7A.png

這是一個商品上架的流程:建立商品主檔(T1)、設定價格(T2)、發佈到前台(T3)。
以上步驟屬於不同服務、不同資料庫,無法依靠單一交易機制保證全有或全無。

https://ithelp.ithome.com.tw/upload/images/20250928/20141146CWqc8QE9h1.png

做法:把大交易拆成多個 Transaction。若某一步失敗,就透過補償(compensation)反向操作先前「已完成」的步驟。
紅色節點代表補償,它不是「資料庫的 rollback」,而是「業務邏輯的反向動作」。
例如 T3 發佈到前台失敗,就執行 revert pricing 與 delete product。

在 Temporal 中實作 Saga

在 Temporal 中,可以非常簡單的用 Workflow 來編排流程,並以 Saga 物件管理補償:

  • 建立 Saga 物件。
  • 每個步驟成功後立刻 addCompensation 登記對應的反向操作。
  • 若發生錯誤,呼叫 saga.compensate() 依相反順序逐一執行補償。
  • 設定重試,讓流程在條件下可以順利度過暫時性的錯誤

程式碼範例

這裡僅列出 ProductOnboardingWorkflowImpl 作為代表:

package com.flowzati.process2;

import io.temporal.activity.ActivityOptions;
import io.temporal.common.RetryOptions;
import io.temporal.workflow.Saga;
import io.temporal.workflow.Workflow;

import java.time.Duration;

public class ProductOnboardingWorkflowImpl implements ProductOnboardingWorkflow {
  private final ProductActivities acts;

  public ProductOnboardingWorkflowImpl() {
    // 設定重試,讓流程在條件下可以順利度過暫時性的錯誤
    RetryOptions retry = RetryOptions.newBuilder()
        .setMaximumAttempts(2) 
        .setInitialInterval(Duration.ofSeconds(5))
        .setBackoffCoefficient(2.0)
        .build();

    ActivityOptions ao = ActivityOptions.newBuilder()
        .setRetryOptions(retry)
        .setScheduleToCloseTimeout(Duration.ofSeconds(60))
        .build();

    this.acts = Workflow.newActivityStub(ProductActivities.class, ao);
  }

  @Override
  public ProductPublishResult run(ProductInput input) {
    Saga.Options sagaOptions = new Saga.Options.Builder().setParallelCompensation(false).build();
    Saga saga = new Saga(sagaOptions);
    try {
      // Step 1: 建商品檔
      String productId = acts.createOrUpdateProduct(input);
      // 立刻 addCompensation 登記對應的反向操作 刪除商品檔。
      saga.addCompensation(() -> acts.deleteProduct(productId));

      // Step 2: 設定價格與可銷售
      acts.setupPricingAndAvailability(productId, input);
      // 立刻 addCompensation 登記對應的反向操作 刪除價格與可銷售紀錄。
      saga.addCompensation(() -> acts.revertPricingAndAvailability(productId));

      // Step 3: 發佈至商店頁面
      acts.publishToStorefront(productId, input);
      // 立刻 addCompensation 登記對應的反向操作 取消發佈至前台。
      saga.addCompensation(() -> acts.unpublishFromStorefront(productId));

      return new ProductPublishResult(productId, true, "Product published via STP");
    } catch (Exception e) {
      // 如果發生問題,只要判斷呼叫 saga.compensate(),就會按照相反的順序,一一執行反向操作。
      saga.compensate();
      return new ProductPublishResult(false, "Failed to publish product. Compensation executed: " + e.getMessage());
    }
  }
}

上述程式展現了 Saga 的核心邏輯:

  • 「每個小步驟都先提交,若後面出問題,再用補償一步步反向操作」。
  • 補償動作同樣要遵守冪等性,確保重試安全。

最佳實務與常見陷阱

  • 冪等性優先:利用業務鍵(如 productIdrequestId)避免重複生效。
  • 補償可重入:補償邏輯必須可多次執行且安全(不存在就忽略)。
  • 優先驗證不可逆步驟:將不可逆或昂貴動作放後面,或加入審批與人工閘道。
  • 控制重試風暴:為外部系統設計合理的 backoff、超時與熔斷。
  • 觀測與審計:關聯 ID、事件歷史、統計與告警,縮短問題定位時間。

結尾

Saga Pattern 用 Temporal 統一編排流程來實施分散式交易,並在實作時聚焦步驟切分、補償、重試、冪等來達到最終一致性。

下一篇我們來討論如何透過 Temproal 完成一個互動式的流程。


上一篇
Day13 - 流量高峰不失控:併發與限速的設計指南
系列文
Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言