2. 任务编排核心概念:DAG、串并行编排、扇入扇出与状态机模型

在异步任务编排系统中,核心挑战在于如何描述任务之间的依赖关系、执行顺序以及运行时状态转换。本章将系统讲解构成编排引擎的四大基石:DAG(有向无环图)、串行与并行编排、扇入与扇出模式以及任务状态机模型。理解这些概念是设计可靠补偿机制与链路追踪的前提。

2.1 DAG:任务依赖关系的数学基础

DAG(Directed Acyclic Graph)是任务编排最通用的抽象模型。每个任务是一个节点,任务间的依赖关系是一条有向边,且图中不存在循环依赖。DAG 保证了任务可以按照拓扑顺序执行,不会出现死锁。

核心特性: DAG 的“无环”约束是异步编排的底线——一旦出现环,补偿逻辑将无法确定回滚顺序,链路追踪也会产生无限循环。

下表对比了 DAG 与其他编排模型的适用场景:

模型 依赖表达力 循环支持 拓扑排序 典型场景
DAG 强(多对多) 否 必需 微服务编排、工作流引擎
树形 弱(一对多) 否 简单 批处理任务、MapReduce
有环图 强 是 不支持 状态机、重试循环(需特殊处理)

以下 SVG 展示了一个典型的订单履约 DAG,包含支付、库存、物流三个子流程的依赖关系:

订单履约 DAG 依赖示例 订单履约 DAG 依赖示例 创建订单 支付扣款 库存锁定 物流下单 履约完成 说明:支付、库存、物流三个子任务并行执行,全部完成后触发“履约完成”。 任意子任务失败时,DAG 引擎需触发已成功子任务的补偿操作。

2.2 串行与并行编排

DAG 的边定义了两种基本执行模式:串行(顺序依赖)和并行(无依赖或扇出)。编排引擎需要根据 DAG 的拓扑排序结果,动态决定哪些任务可以并发执行。

2.2.1 串行编排

当任务 B 依赖任务 A 的输出时,必须串行执行。串行链路的补偿顺序与正向执行顺序相反——即“后进先出”的回滚策略。

实践要点: 串行链路的补偿必须严格逆序。例如 A→B→C 执行成功后,若 C 的业务校验失败,补偿顺序应为 C→B→A。编排引擎需维护一个“执行栈”来记录正向顺序。

2.2.2 并行编排

当多个任务之间不存在依赖关系时,引擎可以同时调度它们。并行编排能显著降低整体延迟,但会引入资源竞争和部分失败的复杂性。

以下代码片段展示了使用 CompletableFuture 实现并行编排的典型模式:

// Java 并行编排示例:同时执行库存锁定和支付扣款
CompletableFuture<Boolean> stockFuture = CompletableFuture
    .supplyAsync(() -> inventoryService.lock(productId, quantity));

CompletableFuture<Boolean> paymentFuture = CompletableFuture
    .supplyAsync(() -> paymentService.charge(orderId, amount));

// 等待所有并行任务完成
CompletableFuture<Void> allOf = CompletableFuture
    .allOf(stockFuture, paymentFuture);

// 聚合结果,处理部分失败
allOf.thenAccept(v -> {
    boolean stockOk = stockFuture.join();
    boolean paymentOk = paymentFuture.join();
    if (!stockOk || !paymentOk) {
        // 触发补偿:已成功的任务需要回滚
        if (stockOk) inventoryService.unlock(productId, quantity);
        if (paymentOk) paymentService.refund(orderId, amount);
    }
});

2.3 扇入与扇出模式

扇出(Fan-out)和扇入(Fan-in)是并行编排的两种典型拓扑结构,在分布式系统中极为常见。

模式 定义 补偿策略 典型场景
扇出 一个父任务分发到多个子任务并行执行 子任务独立补偿,父任务需等待所有子任务完成或失败 批量发送通知、多仓库库存查询
扇入 多个子任务的结果汇聚到一个父任务 父任务失败时,需补偿所有已成功的子任务 聚合报表、多数据源合并

下面的 SVG 图展示了扇出与扇入的对比结构:

扇出与扇入模式对比 扇出与扇入模式对比 扇出 (Fan-out) 分发任务 子任务 A 子任务 B 子任务 C 扇入 (Fan-in) 数据源 1 数据源 2 聚合任务 扇出补偿要点: 分发任务失败时,所有已启动的子任务必须独立补偿。 扇入补偿要点: 聚合任务失败时,需补偿所有已成功的数据源任务。 两种模式都要求编排引擎记录每个子任务的执行状态,以便精确补偿。 实际系统中,扇出和扇入经常嵌套出现,形成复杂的多层 DAG。

2.4 任务状态机模型

每个任务实例在生命周期中都会经历一系列状态转换。标准的状态机模型是编排引擎实现幂等补偿和断点续传的基础。

2.4.1 标准状态定义

状态 含义 可转换至 补偿行为
PENDING 任务已创建,等待调度 RUNNING, SKIPPED 无需补偿
RUNNING 任务正在执行 SUCCESS, FAILED, TIMEOUT 需等待最终状态
SUCCESS 任务执行成功 COMPENSATING(仅当上游失败时) 可被补偿
FAILED 任务执行失败 RETRYING, COMPENSATED 触发重试或补偿
COMPENSATING 正在执行补偿逻辑 COMPENSATED, COMP_FAILED 补偿中
COMPENSATED 补偿成功 终态 已完成
COMP_FAILED 补偿失败 终态(需人工介入) 需告警

2.4.2 状态转换图

以下 SVG 展示了任务状态机的完整转换路径,重点标注了补偿相关的状态分支:

任务状态机模型 任务状态机模型(含补偿分支) PENDING RUNNING SUCCESS FAILED COMPENSATING COMPENSATED 调度 成功 失败 上游失败触发补偿 重试 补偿完成 虚线表示可选路径:SUCCESS 可进入 COMPENSATING(当依赖的上游任务失败时);FAILED 可进入重试循环。
状态机设计陷阱: 不要将 COMPENSATING 和 COMPENSATED 设计为全局终态。每个任务实例的补偿状态应独立维护,避免因一个任务的补偿失败导致整个 DAG 的补偿链路卡死。建议引入“补偿超时”和“人工干预”状态作为兜底。

2.5 综合案例:电商下单 DAG 编排

将上述概念整合到一个实际案例中。以下是一个电商下单场景的 DAG 编排定义,包含串行、并行、扇入扇出以及完整的状态转换:

// 伪代码:电商下单 DAG 编排定义
DAG orderDag = DAG.builder()
    .addNode("validate", TaskType.SERIAL)          // 校验参数
    .addNode("reserveStock", TaskType.PARALLEL)    // 锁定库存(扇出到多仓)
    .addNode("chargePayment", TaskType.SERIAL)     // 扣款
    .addNode("delivery", TaskType.PARALLEL)        // 物流下单(扇出到多家物流)
    .addNode("notify", TaskType.FAN_IN)            // 通知用户(扇入)
    .addEdge("validate", "reserveStock")
    .addEdge("validate", "chargePayment")
    .addEdge("reserveStock", "delivery")
    .addEdge("chargePayment", "delivery")
    .addEdge("delivery", "notify")
    .build();

// 补偿策略:逆序补偿,每个节点独立状态机
orderDag.onFailure(node -> {
    if (node.getState() == TaskState.SUCCESS) {
        node.compensate();  // 触发补偿逻辑
    }
});

该案例中:

  • validate 为串行前置节点,失败则整个 DAG 终止。
  • reserveStock 和 chargePayment 并行执行,互不依赖。
  • delivery 依赖库存和支付都成功后执行,属于扇入。
  • notify 为最终扇入节点,聚合所有前置结果。
  • 任意节点失败时,仅补偿已成功的上游节点,未执行的节点直接标记为 SKIPPED。
核心结论: DAG 提供了依赖关系的数学保证,串行/并行编排决定了执行效率,扇入/扇出模式描述了数据流拓扑,状态机模型则为每个任务提供了可追溯的生命周期。四者结合,构成了异步任务编排错误处理与补偿机制的底层骨架。