4. 工作流编排模式:顺序编排、并行编排、子工作流编排、条件分支编排在DAG边界中的实现
Temporal工作流引擎将业务逻辑建模为有向无环图(DAG),其中每个节点代表一个活动(Activity)或子工作流(Child Workflow),边代表执行依赖与数据传递。在DAG边界内,编排模式决定了节点间的拓扑关系与调度策略。本章深入分析四种核心编排模式在Temporal中的实现机制、代码结构及数据一致性保障要点。
4.1 顺序编排:线性依赖与确定性重放
顺序编排是最基础的DAG拓扑,节点按严格线性顺序执行,前一个节点完成后才能启动下一个节点。Temporal通过工作流代码的同步调用天然实现顺序编排,无需显式定义依赖关系。
ActivityStub.execute() 调用都是阻塞的,Temporal SDK在重放时通过事件历史中的标记(Marker)确定执行顺序,保证确定性。
// 顺序编排示例:订单处理流程
public void executeOrder(OrderInput input) {
// 步骤1:验证订单
String validationResult = activities.validateOrder(input);
// 步骤2:扣减库存(依赖步骤1完成)
String inventoryResult = activities.deductInventory(input);
// 步骤3:生成发货单(依赖步骤2完成)
String shippingResult = activities.createShipping(input);
// 步骤4:发送通知(依赖步骤3完成)
activities.sendNotification(input);
}
在DAG边界视角下,顺序编排的每个节点仅有一个前驱和一个后继,形成一条链式路径。Temporal的事件历史会记录每个活动的调度与完成事件,重放时严格按照事件顺序执行。
@Signal 或 @Query 方法获取中间状态,而非依赖活动返回值链式传递。
4.2 并行编排:扇出/扇入与一致性边界
并行编排允许DAG中多个节点同时执行,通过 Promise 或 CompletableFuture 实现扇出(Fan-out)与扇入(Fan-in)模式。Temporal的并行编排在DAG边界内必须保证所有分支完成后再继续后续节点。
// 并行编排示例:批量数据同步
public void syncBatchData(BatchInput input) {
List<Promise<String>> promises = new ArrayList<>();
// 扇出:并行执行三个数据同步活动
promises.add(Async.function(activities::syncUserData, input));
promises.add(Async.function(activities::syncOrderData, input));
promises.add(Async.function(activities::syncInventoryData, input));
// 扇入:等待所有并行分支完成
Promise.allOf(promises).get();
// 所有分支完成后执行汇总
activities.aggregateSyncResult(input);
}
| 并行模式 | 实现方式 | DAG边界特征 | 一致性要求 |
|---|---|---|---|
| 纯并行 | Async.function() + Promise.allOf() |
多个节点无依赖,同时执行 | 各分支独立事务,最终一致性 |
| 限流并行 | 自定义信号量 + Async.function() |
控制并发度,避免资源耗尽 | 需考虑限流状态的一致性 |
| 动态并行 | 循环 + Async.function() |
运行时确定并行节点数量 | 动态分支的幂等性保障 |
Promise.allOf() 会抛出异常。此时已成功完成的分支活动不会自动回滚,需要在工作流代码中实现补偿逻辑(Saga模式)或使用Temporal的 @ActivityMethod(heartbeatTimeout) 配合超时重试。
4.3 子工作流编排:DAG嵌套与状态隔离
子工作流(Child Workflow)允许将复杂DAG拆分为多个独立的工作流实例,形成嵌套DAG结构。每个子工作流拥有独立的事件历史、重放上下文和错误处理边界,父工作流通过 ChildWorkflowStub 进行编排。
// 子工作流编排示例:多阶段审批流程
public void executeApprovalProcess(ApprovalInput input) {
// 创建子工作流存根
ChildWorkflowStub stage1Stub = Workflow.newChildWorkflowStub(
Stage1Workflow.class,
ChildWorkflowOptions.newBuilder()
.setWorkflowId("stage1-" + input.getRequestId())
.setTaskQueue("approval-task-queue")
.build()
);
// 执行第一阶段审批(子工作流)
Promise<Stage1Result> stage1Result = Async.function(
stage1Stub::execute, input.getStage1Data()
);
// 等待第一阶段完成
Stage1Result result1 = stage1Result.get();
// 根据第一阶段结果决定是否进入第二阶段
if (result1.isApproved()) {
ChildWorkflowStub stage2Stub = Workflow.newChildWorkflowStub(
Stage2Workflow.class,
ChildWorkflowOptions.newBuilder()
.setWorkflowId("stage2-" + input.getRequestId())
.build()
);
stage2Stub.execute(input.getStage2Data());
}
}
| 特性 | 子工作流 | 活动(Activity) | DAG边界影响 |
|---|---|---|---|
| 事件历史 | 独立 | 属于父工作流 | 子工作流DAG边界独立 |
| 重放范围 | 子工作流独立重放 | 父工作流重放时重放 | 子工作流失败不影响父工作流历史 |
| 错误处理 | 可独立重试/补偿 | 由父工作流处理 | 子工作流DAG边界内可自定义策略 |
| 状态隔离 | 完全隔离 | 无状态 | 子工作流DAG边界内状态对外不可见 |
4.4 条件分支编排:动态路径选择与DAG拓扑变化
条件分支编排根据运行时数据或外部信号动态选择DAG执行路径。Temporal通过工作流代码中的 if-else、switch 或 @Signal 条件判断实现分支,DAG拓扑在重放时由事件历史中的决策事件确定。
// 条件分支编排示例:风险控制流程
public void executeRiskControl(RiskInput input) {
// 执行风险评估活动
RiskLevel riskLevel = activities.evaluateRisk(input);
// 条件分支:根据风险等级选择不同路径
if (riskLevel == RiskLevel.LOW) {
// 低风险:自动通过
activities.autoApprove(input);
} else if (riskLevel == RiskLevel.MEDIUM) {
// 中风险:人工审核
activities.manualReview(input);
} else {
// 高风险:拒绝并通知
activities.rejectRequest(input);
activities.notifyCompliance(input);
}
// 所有分支汇聚到最终节点
activities.completeProcess(input);
}
| 分支类型 | 触发条件 | DAG拓扑特征 | 一致性保障 |
|---|---|---|---|
| 数据驱动分支 | 活动返回值 | 静态分支结构,运行时选择 | 重放时活动返回值必须一致 |
| 信号驱动分支 | 外部 @Signal |
动态分支,信号到达后决定路径 | 信号事件在历史中持久化 |
| 超时驱动分支 | Workflow.sleep() 或定时器 |
时间条件触发分支切换 | 定时器事件保证确定性 |
4.5 编排模式组合:复杂DAG边界的构建策略
实际业务场景中,四种编排模式通常组合使用。Temporal工作流通过嵌套、串联、并联等方式构建复杂DAG边界。以下表格总结了组合策略与一致性保障要点:
| 组合模式 | 典型场景 | DAG边界复杂度 | 一致性策略 |
|---|---|---|---|
| 顺序 + 并行 | 数据批处理:先验证,再并行处理各批次 | 中等 | 并行分支独立事务,最终汇聚 |
| 并行 + 条件分支 | 多维度风险评估:并行评估后根据结果分支 | 高 | 并行结果汇聚后作为分支条件 |
| 子工作流 + 顺序 | 多阶段审批:每个阶段为子工作流,顺序执行 | 高 | 子工作流独立重试,父工作流控制顺序 |
| 子工作流 + 并行 | 多区域数据同步:每个区域为子工作流,并行执行 | 极高 | 子工作流边界隔离,父工作流扇入汇聚 |
// 组合编排示例:多区域数据同步 + 条件分支
public void executeMultiRegionSync(SyncInput input) {
// 阶段1:顺序执行全局验证
activities.validateGlobalConfig(input);
// 阶段2:并行执行各区域子工作流
List<Promise<RegionResult>> regionPromises = new ArrayList<>();
for (Region region : input.getRegions()) {
ChildWorkflowStub regionStub = Workflow.newChildWorkflowStub(
RegionSyncWorkflow.class,
ChildWorkflowOptions.newBuilder()
.setWorkflowId("region-sync-" + region.getId())
.build()
);
regionPromises.add(Async.function(regionStub::execute, region));
}
// 等待所有区域同步完成
List<RegionResult> results = Promise.allOf(regionPromises).get();
// 阶段3:条件分支 - 根据同步结果决定后续
boolean allSuccess = results.stream().allMatch(RegionResult::isSuccess);
if (allSuccess) {
activities.notifySyncComplete(input);
} else {
activities.triggerRollback(input);
}
}
- 最小边界原则: 每个DAG边界内只包含一个业务阶段,避免边界过大导致重放性能下降。
- 边界隔离原则: 子工作流边界提供强隔离,适合需要独立重试策略或不同团队维护的模块。
- 确定性原则: 所有DAG边界内的分支判断必须基于确定性数据,确保重放一致性。
- 幂等性原则: 边界内的每个活动必须实现幂等性,支持重复执行不产生副作用。
通过合理组合四种编排模式,Temporal工作流能够构建出满足复杂业务需求的DAG边界,同时保持数据一致性与确定性重放能力。在实际架构设计中,应根据业务场景的复杂度、一致性要求和性能指标选择合适的编排模式组合。