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触发,调用方不阻塞。适用于松耦合、长等待或外部事件驱动的场景。
3.5 DAG 设计中的常见陷阱与避坑
陷阱1:隐式循环依赖
虽然 Temporal 禁止 Workflow 内出现死循环,但开发者可能在多个 Child Workflow 之间形成循环调用(A→B→C→A)。这种循环在 DAG 语义下是非法的,会导致 Workflow 永远无法完成。解决方案:在架构设计阶段绘制完整的 DAG 依赖图,使用工具(如 Graphviz)自动检测环。
虽然 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 同时启动超过 100 个并行 Activity 时,Event History 会急剧增长,可能触发 50KB 事件大小限制。解决方案:使用 扇出限流 模式,将大规模并行拆分为多个批次,每个批次作为一个子 DAG 由 Child Workflow 管理。
最佳实践: 在 Workflow 代码开头添加一个
@SignalMethod 用于接收“取消”指令,并在每个 DAG 节点执行前检查该信号。这样可以在不破坏 DAG 结构的前提下,实现优雅的流程终止。
3.6 小结
DAG 工作流模型在 Temporal 中通过 Workflow 代码的自然编排实现,无需额外框架。开发者应理解三种 DAG 表示形态(隐式、显式、嵌套),并根据服务边界粒度选择合适的通信机制(同步调用 vs 异步 Signal)。在划分 DAG 边界时,遵循“高内聚、低耦合”原则,将频繁变动的子流程封装为 Child Workflow,将无状态任务封装为 Activity,从而构建可演化、可观测的分布式系统。