iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Software Development

Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程系列 第 9

Day09 - 不管是一次、一百次還是一萬次,我還是想跟你說我愛...冪等性

  • 分享至 

  • xImage
  •  

Retry / At-Least-Once

依照現有的範例,註冊流程通常會拆分成多個服務:

  1. 建立帳號
  2. 送點數 500 點
  3. 寄出歡迎信

這個看似簡單的流程,在分散式環境中還隱藏著另一個重要問題。

當暫時性的失敗發生時,我們需要 Retry 機制讓流程回復執行,

但加了 Retry,就等於同一個動作可能被執行一次或多次,也就是 At-Least-Once 的執行語意。

重試災難場景

想像一下註冊流程的這個場景:

  • 用戶 John 註冊帳號,workflow ID 為 registration-uuid
  • Step 1: 建立帳號 ✅ 成功
  • Step 2: 送點數 - 網路延遲,操作 timeout
  • Temporal 觸發 retry
  • Step 2 重試: 又送了一次 500 點!😱
  • Step 3: 寄出歡迎信 - 又因為 timeout 重試
  • Step 3 重試: John 收到兩封歡迎信!😱

結果:John 的帳戶有 1000 點(應該只有 500 點),還收到重複的歡迎信。

這也就是思考冪等性的價值所在。接下來,我們來看冪等性在設計上的核心原則。

冪等性的核心原則

在分散式流程中實作冪等性,重點不是「不重試」,而是「重試多少次都不會造成副作用重複」。

設計建議:

  1. 使用唯一的冪等鍵:設計跨服務可傳遞的冪等鍵(idempotency key),通常以 workflowId 為核心再加上操作類型。
  2. 先檢查再執行在執行副作用前,先以冪等鍵進行「去重檢查」。
  3. 記錄操作結果:第一次執行才產生副作用,並把結果持久化;之後重複請求直接返回既有結果。
  4. 返回一致結果:讓 Workflow 產生且管理這些鍵,確保同一條業務路徑下的鍵完全一致、可重放。

註冊流程的冪等性解決方案

觸發啟動流程時,帶上唯一的冪等鍵:

public class Process1StartTrigger {
  public static void main(String[] args) {
    WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
    WorkflowClient client = WorkflowClient.newInstance(service);

    String userId = UUID.randomUUID().toString();
    String email = "flow@gmail.com";

    RegistrationWorkflow workflow = client.newWorkflowStub(
        RegistrationWorkflow.class,
        WorkflowOptions.newBuilder()
            .setTaskQueue(TASK_QUEUE)
            // 流程帶上唯一的 workflow ID
            .setWorkflowId("registration-" + userId)
            .build());

    RegisterRequest request = new RegisterRequest(userId, email);
    WorkflowExecution execution = WorkflowClient.start(workflow::register, request);
    System.out.println("WorkflowId: " + execution.getWorkflowId());
    System.out.println("RunId: " + execution.getRunId());
  }
}

Activity 的輸入參數都帶上冪等鍵,作為操作的去重檢查。

public class RegistrationActivitiesImpl implements RegistrationActivities {
  // Demo 用:記憶體模擬「資料庫」
  private final Set<String> users = ConcurrentHashMap.newKeySet();
  private final Set<String> ops = ConcurrentHashMap.newKeySet(); // 記錄 points/Email 的 idempotency key

  @Override
  public void createAccount(RegisterRequest req) {
    // 冪等:userId 唯一(真實情況請改為 DB 唯一鍵/UPSERT)
    boolean added = users.add(req.getUserId());
    if (!added) {
      return; // 已存在 → 當成成功返回(冪等)
    }
    // 這裡可寫入 DB:INSERT users(id, email) ON CONFLICT DO NOTHING
    simulateExtraInvoke(5);
    System.out.println("Created account for " + req.getUserId() + " (" + req.getEmail() + ")");
  }

  @Override
  public void addPoints(String userId, int points, String opKey) {
    // 冪等保護:同一筆加點操作以 (POINTS, userId, opKey) 唯一
    String dedupeKey = "POINTS#" + userId + "#" + opKey;
    boolean firstTime = ops.add(dedupeKey);
    if (!firstTime) {
      System.out.println("[idempotent] addPoints skipped for " + userId + " [op=" + opKey + "]");
      return;
    }

    // 只有第一次才會執行副作用(例如:UPDATE balance = balance + points)
    simulateExtraInvoke(5);
    System.out.println("Added " + points + " points to " + userId + " [op=" + opKey + "]");
  }

  @Override
  public void sendEmail(String email, String messageId) {
    // 冪等保護:同一封信以 messageId 唯一
    String dedupeKey = "EMAIL#" + messageId;
    boolean firstTime = ops.add(dedupeKey);
    if (!firstTime) {
      System.out.println("[idempotent] sendEmail skipped for " + email + " [msgId=" + messageId + "]");
      return;
    }

    // 只有第一次才會執行副作用(例如:呼叫外部信件服務)
    simulateExtraInvoke(5);
    System.out.println("Sent welcome email to " + email + " [msgId=" + messageId + "]");
  }

  /** 模擬外部呼叫耗時(僅作示範) */
  private static void simulateExtraInvoke(int timeoutSeconds) {
    try {
      TimeUnit.SECONDS.sleep(timeoutSeconds);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }
}

最佳實務

  • 請務必從 Workflow 產生冪等鍵,並在 Activity 及外部系統完整傳遞。
  • 對外部系統(例如 Email/SMS/金流)盡量利用它們的去重能力(Idempotency-KeyMessage-Id)。
  • DB 端用唯一鍵或 UPSERT 實作去重;必要時寫入「已處理事件」表。
  • 規劃冪等鍵保存期限與清理策略(永續 vs 有效視窗)。

這樣設計後,不管網路問題導致多少次重試,John 永遠只會:

  • 有一個帳號
  • 獲得 500 點(不是 1000 點)
  • 收到一封歡迎信(不是兩封)

結語

在這篇,我們看到冪等性如何確保流程在重試下仍然正確。
但在實務中,不只有「重試」會造成流程失敗,還有「無法恢復的錯誤」。

下一篇我們就來談談錯誤處理策略,看看怎麼在流程中面對錯誤。


上一篇
Day08 - 分散式流程的順序性(Ordering)挑戰
系列文
Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言