依照現有的範例,註冊流程通常會拆分成多個服務:
這個看似簡單的流程,在分散式環境中還隱藏著另一個重要問題。
當暫時性的失敗發生時,我們需要 Retry 機制讓流程回復執行,
但加了 Retry,就等於同一個動作可能被執行一次或多次,也就是 At-Least-Once 的執行語意。
想像一下註冊流程的這個場景:
registration-uuid
結果:John 的帳戶有 1000 點(應該只有 500 點),還收到重複的歡迎信。
這也就是思考冪等性的價值所在。接下來,我們來看冪等性在設計上的核心原則。
在分散式流程中實作冪等性,重點不是「不重試」,而是「重試多少次都不會造成副作用重複」。
設計建議:
workflowId
為核心再加上操作類型。觸發啟動流程時,帶上唯一的冪等鍵:
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();
}
}
}
Idempotency-Key
、Message-Id
)。這樣設計後,不管網路問題導致多少次重試,John 永遠只會:
在這篇,我們看到冪等性如何確保流程在重試下仍然正確。
但在實務中,不只有「重試」會造成流程失敗,還有「無法恢復的錯誤」。
下一篇我們就來談談錯誤處理策略,看看怎麼在流程中面對錯誤。