3. DAG 工作流模型:有向无环图在 Temporal 中的表示方式

有向无环图(DAG)是描述任务依赖关系的核心数学模型。在 Temporal 中,Workflow 代码天然具备 DAG 表达能力——每个 Workflow 函数体中的步骤调用顺序、条件分支、并行扇出与汇聚,共同构成一张隐式或显式的 DAG。理解 DAG 在 Temporal 中的表示方式,是划分服务边界与设计通信机制的基础。

3.1 DAG 的基本要素与 Temporal 映射

DAG 要素 定义 Temporal 中的对应实现
节点(Node) 一个独立执行单元,无副作用或可重入 Activity 或 ChildWorkflow 调用
边(Edge) 节点间的依赖关系,表示执行顺序或数据传递 Workflow 代码中的同步调用、Future.get()、Async.function()
入度(In-degree) 指向某节点的前置依赖数量 调用前等待多个 Promise 完成
出度(Out-degree) 某节点出发的后继分支数量 扇出(Fan-out)模式:同时启动多个 Activity
无环(Acyclic) 不存在循环依赖路径 Temporal 禁止 Workflow 内出现死循环调用链;循环需通过 continueAsNew 或显式状态机实现
关键原则: Temporal 不要求开发者显式构建 DAG 数据结构。开发者只需用顺序代码描述逻辑,Temporal 的 Event History 会自动记录执行轨迹,并在恢复时按 DAG 语义重放。

3.2 Workflow 作为 DAG 节点的执行逻辑

在 Temporal 中,每个 Workflow 本身可以视为 DAG 中的一个“复合节点”。一个 Workflow 内部可以包含多个 Activity 子节点,也可以启动 Child Workflow 形成嵌套 DAG。这种分层结构让服务边界划分更加灵活:

  • 顶层 Workflow:代表整个业务用例的 DAG 根节点,负责编排全局流程。
  • Activity 节点:无状态、幂等的任务单元,对应微服务中的单个操作(如“扣减库存”“发送通知”)。
  • Child Workflow 节点:有状态的子流程,对应一个独立的子领域边界(如“订单履约流程”“退款审批流程”)。
// 示例:一个典型的 DAG 风格 Workflow
public class OrderWorkflowImpl implements OrderWorkflow {

    @Override
    public OrderResult execute(OrderInput input) {
        // 节点1:校验订单(Activity)
        ValidationResult valid = activity.validate(input);

        // 节点2 & 3:并行扇出——扣库存 & 预授权支付
        Promise<InventoryResult> inv = Async.function(activity::deductInventory, input);
        Promise<PaymentResult> pay = Async.function(activity::authorizePayment, input);

        // 汇聚点:等待两个并行节点完成
        InventoryResult invResult = inv.get();
        PaymentResult payResult = pay.get();

        // 节点4:根据结果决定后续路径(条件分支)
        if (invResult.isSuccess() && payResult.isSuccess()) {
            // 节点5:创建发货单(Child Workflow 节点)
            return childWorkflow.createShipment(input);
        } else {
            // 节点6:补偿——取消预授权(Activity 节点)
            activity.cancelPayment(payResult.getTransactionId());
            return OrderResult.failed("库存或支付失败");
        }
    }
}

3.3 DAG 在 Temporal 中的三种表示形态

根据业务复杂度,DAG 在 Temporal 中可以有三种不同的表示形态,开发者应根据服务边界粒度选择:

形态 描述 适用场景 边界粒度
隐式 DAG Workflow 代码中的顺序调用自然形成 DAG,无需额外结构 简单线性流程(如“创建订单→扣库存→发通知”) 粗粒度(单个微服务内部)
显式 DAG 使用 List<Promise>、CompletableFuture 或自定义 DAG 解析器编排 复杂并行/条件流程(如“风控审核→并行调用三个外部服务→汇总结果”) 中粒度(跨多个 Activity)
嵌套 DAG Child Workflow 作为子 DAG 节点,形成递归分层结构 跨服务/跨团队的长流程(如“电商下单→支付→物流→售后”) 细粒度(每个子 DAG 对应一个独立服务边界)
实践建议: 当 DAG 深度超过 3 层或节点数超过 20 时,优先考虑将部分子图抽取为 Child Workflow。这不仅能降低单个 Workflow 的 Event History 大小,还能让每个子 DAG 拥有独立的版本管理和错误域。

3.4 DAG 边界划分与通信机制

DAG 中的每条边都代表一次跨节点通信。在 Temporal 中,这种通信机制分为两类:

  • 同步通信(边为实线):通过 ActivityStub 或 ChildWorkflowStub 直接调用,调用方阻塞等待结果。适用于强依赖、低延迟的场景。
  • 异步通信(边为虚线):通过 Signal 或 Callback 触发,调用方不阻塞。适用于松耦合、长等待或外部事件驱动的场景。
Temporal DAG 边界划分与通信机制 顶层 Workflow 同步调用(Activity) 同步调用(Activity) 节点A:扣减库存 节点B:预授权支付 汇聚:等待结果 成功 失败 子DAG:创建发货单 补偿:取消支付 异步 Signal:通知外部系统 Activity 节点 Child Workflow 节点 补偿节点 同步边 异步边(Signal)

3.5 DAG 设计中的常见陷阱与避坑

陷阱1:隐式循环依赖
虽然 Temporal 禁止 Workflow 内出现死循环,但开发者可能在多个 Child Workflow 之间形成循环调用(A→B→C→A)。这种循环在 DAG 语义下是非法的,会导致 Workflow 永远无法完成。解决方案:在架构设计阶段绘制完整的 DAG 依赖图,使用工具(如 Graphviz)自动检测环。
陷阱2:扇出过度导致 Event History 膨胀
当单个 Workflow 同时启动超过 100 个并行 Activity 时,Event History 会急剧增长,可能触发 50KB 事件大小限制。解决方案:使用 扇出限流 模式,将大规模并行拆分为多个批次,每个批次作为一个子 DAG 由 Child Workflow 管理。
最佳实践: 在 Workflow 代码开头添加一个 @SignalMethod 用于接收“取消”指令,并在每个 DAG 节点执行前检查该信号。这样可以在不破坏 DAG 结构的前提下,实现优雅的流程终止。

3.6 小结

DAG 工作流模型在 Temporal 中通过 Workflow 代码的自然编排实现,无需额外框架。开发者应理解三种 DAG 表示形态(隐式、显式、嵌套),并根据服务边界粒度选择合适的通信机制(同步调用 vs 异步 Signal)。在划分 DAG 边界时,遵循“高内聚、低耦合”原则,将频繁变动的子流程封装为 Child Workflow,将无状态任务封装为 Activity,从而构建可演化、可观测的分布式系统。