容易犯錯的順序性問題,核心在於:
常見誤解 | 真實分散式行為 | 典型後果 |
---|---|---|
網路/佇列 (Queue) 會保證全域順序(「我先發 A,再發 B,對方一定先看到 A 再看到 B」) | 網路封包可能亂序抵達;MQ 只保證 partition 內有序,跨 partition 無序 | 訊息 B 比 A 先處理,流程語意錯亂 |
跨服務流程等同單機線性執行(「呼叫 A → B → C,一定照順序跑完」) | A、B、C 可能因延遲/重試/並行導致先後顛倒 | 先寄歡迎信再建帳號、先扣庫存再下單 |
最終一致性 = 最後一定正確(「就算慢一點,狀態最後會對」) | 如果事件 replay/複製順序錯誤,狀態可能「一致但錯誤」 | Event Sourcing 重播失敗、投影資料錯誤 |
那麼有什麼解決辦法呢?
層次 | 技術範例 | 保序範圍 | 邏輯維護成本 | 吞吐量影響 | 延遲影響 | 基礎設施成本 | 適用場景建議 |
---|---|---|---|---|---|---|---|
流程語意層 (Workflow) | Temporal Workflow、Camunda Process Instance | 單一流程的事件/步驟執行順序 | 低:邏輯直觀,引擎管理 replay;但需撰寫補償邏輯,遵守 deterministic 限制 | 單流程:低吞吐;全系統:高吞吐(多流程並行) | 步驟累積 + replay → 延遲偏高 | 高:需 Workflow 引擎 | 跨服務業務流程(下單、補償交易、長流程) |
單一責任序列化層 (Actor / Entity Workflow) | Temporal Entity Pattern、Akka Actor單執行緒消費者 | 同一資源的狀態轉換執行順序 | 低:邏輯直觀,框架保序;但需正確設計 sharding key 避免熱點 | 單 Entity:低吞吐;全系統:高吞吐(可水平擴展) | 單點熱點瓶頸導致排隊延遲上升:隊列排隊時間增加 | 中等:需 actor 系統或 workflow runtime | 銀行帳戶交易、商品庫存操作、使用者錢包 |
事件排序 / 版本控制層 | Event Sourcing (event number)、Optimistic Lock、Lamport/Vector Clock | 事件順序重放執行(同一資源事件需依序處理) | 中等:需維護版本號、處理衝突、理解 replay 與邏輯時鐘 | 單資源:受衝突率影響;全系統:高吞吐(資源可並行) | 固定低延遲:單次版本比對或 log append | 低:僅需現有 DB/Log | 訂單狀態更新、帳戶餘額快照、Event replay |
資料通道層 (Partition 保序) | Kafka Partition、SQS FIFO | 同一 Partition 內的訊息傳輸順序 | 低(單 partition):幾乎零負擔;中(跨 partition):需協調一致性 | 單 Partition:低吞吐;全系統:高吞吐(靠多 Partition 擴展) | 可能因 partition backlog 或 rebalance 增加 | 中高:需要 MQ 基礎設施 | 大規模事件流處理、日誌收集、IoT 資料上傳 |
分散式鎖 (Distributed Lock) | ZooKeeper Lock、etcd lease、Consul session | 同一臨界區的進入順序 | 中等:需處理鎖超時、死鎖、重入鎖等邊界情況 | 全系統:低吞吐(序列化臨界區,無法水平擴展) | 每次 acquire/release 都需共識 → 高延遲 | 高:依賴共識協定 infra | 組態更新、Leader 選舉、全域唯一 ID 產生 |
全域共識層 (Consensus) | Raft、Paxos、ZooKeeper (ZAB) | 全域 Log 的提交順序 | 高:需遵循 log append 模型,並處理 quorum / leader 切換語意 | 全系統:最低吞吐(全域序列化,無法擴展) | 跨節點共識 + 持久化 → 必然高延遲 | 最高:需完整共識協定 | 分散式鎖服務、元資料管理 |
之後會有討論「單一責任序列化層」的篇章,本篇則用註冊流程先討論「流程語意層」的解法。
依照現前的範例,註冊流程通常會拆分成多個服務:
如果只靠訊息隊列和多個 consumer 平行處理,可能出現順序錯亂或狀態不一致:
在 Temporal 中,可以將整個「註冊流程」建模為一個 Registration Workflow:
回頭看一下程式碼
userId
組成)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)
.setWorkflowId("registration-" + userId) // 4. Workflow 唯一 ID
.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());
}
}
流程語意層的保證:
效果:
public class RegistrationWorkflowImpl implements RegistrationWorkflow {
private final RegistrationActivities acts;
@Override
public void register(RegisterRequest req) {
// Step 1: 建帳號
acts.createAccount(req);
// Step 2: 送 500 點
acts.addPoints(req.getUserId(), 500, "signupBonus");
// Step 3: 寄歡迎信
acts.sendEmail(req.getEmail(), "welcome-" + req.getUserId());
}
}
分散式系統的順序性挑戰,是架構設計中的核心課題。唯有先理解問題的根源,再依需求選擇合適的解法,才能打造出既可靠又高效的流程系統。
下一篇我們來討論冪等性問題。