4. 工作流编排模式

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的事件历史会记录每个活动的调度与完成事件,重放时严格按照事件顺序执行。

顺序编排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() 运行时确定并行节点数量 动态分支的幂等性保障
并行编排DAG拓扑:扇出/扇入 批量同步请求 同步用户数据 同步订单数据 同步库存数据 汇总同步结果
一致性陷阱: 并行编排中如果某个分支失败,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边界内状态对外不可见
子工作流编排DAG嵌套结构 父工作流DAG边界 发起审批请求 子工作流1 DAG边界 初审活动A 初审活动B 子工作流2 DAG边界 复审活动C 复审活动D
设计建议: 子工作流适用于以下场景:1)需要独立重试策略的业务阶段;2)需要隔离事件历史避免父工作流历史过大;3)需要复用工作流逻辑的模块化设计。注意子工作流启动有额外开销,高频调用场景应优先使用活动。

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() 或定时器 时间条件触发分支切换 定时器事件保证确定性
条件分支编排DAG拓扑 风险评估 风险等级? 低风险:自动通过 中风险:人工审核 高风险:拒绝+通知 完成处理 LOW MEDIUM HIGH
确定性约束: 条件分支的判断条件必须基于工作流状态中的确定性数据(活动返回值、信号数据、定时器状态)。禁止使用系统时间、随机数、外部API调用结果等非确定性数据作为分支条件,否则会导致重放时选择不同路径,引发数据不一致。

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边界设计原则:
  1. 最小边界原则: 每个DAG边界内只包含一个业务阶段,避免边界过大导致重放性能下降。
  2. 边界隔离原则: 子工作流边界提供强隔离,适合需要独立重试策略或不同团队维护的模块。
  3. 确定性原则: 所有DAG边界内的分支判断必须基于确定性数据,确保重放一致性。
  4. 幂等性原则: 边界内的每个活动必须实现幂等性,支持重复执行不产生副作用。

通过合理组合四种编排模式,Temporal工作流能够构建出满足复杂业务需求的DAG边界,同时保持数据一致性与确定性重放能力。在实际架构设计中,应根据业务场景的复杂度、一致性要求和性能指标选择合适的编排模式组合。