2. 任务编排模式:串行编排、并行编排、DAG(有向无环图)编排的核心思想

在分布式开发环境中,异步任务很少孤立执行。任务之间通常存在依赖关系:某个任务必须等待前置任务完成才能启动,或者多个任务可以同时运行。任务编排模式定义了这些依赖关系的组织方式。理解串行、并行和 DAG 编排是构建可靠异步流水线的基础。

2.1 串行编排(Sequential Orchestration)

串行编排是最直观的模式:任务按照严格的先后顺序依次执行。前一个任务完成后,后一个任务才能开始。这种模式适用于有明确数据依赖或状态依赖的场景。

  • 特点:执行顺序固定,逻辑简单,易于理解和调试。
  • 缺点:整体耗时等于所有任务耗时之和,无法利用并发提升吞吐。
  • 典型场景:用户注册流程(先验证手机号 → 再创建账户 → 最后发送欢迎邮件)。
串行编排的适用判断:如果任务 B 必须使用任务 A 的输出结果作为输入,且任务 C 又依赖任务 B 的结果,则串行是唯一正确的选择。强行并行只会引入竞态条件和数据不一致。

2.2 并行编排(Parallel Orchestration)

并行编排允许没有依赖关系的任务同时执行。在异步任务框架中,并行通常通过线程池、协程或分布式节点实现。并行编排能显著缩短整体执行时间,但需要处理资源竞争和结果聚合。

  • 特点:吞吐量高,整体耗时由最长的分支任务决定。
  • 缺点:需要管理并发控制、资源限制和异常隔离。
  • 典型场景:批量数据清洗(同时处理多个分片)、微服务并行调用(同时查询用户信息、订单信息、库存信息)。
对比维度 串行编排 并行编排
执行顺序 严格有序 无序并发
总耗时 各任务耗时之和 最长分支耗时
资源消耗 低(单线程) 高(多线程/多节点)
错误传播 中断后续任务 仅影响当前分支
结果聚合 天然有序 需要显式合并

2.3 DAG(有向无环图)编排

DAG 编排是串行和并行的泛化形式。它将任务建模为图中的节点,将依赖关系建模为有向边,且图中不允许存在环(即不能出现循环依赖)。DAG 编排能够表达任意复杂的任务依赖拓扑,是工业级异步任务框架(如 Airflow、Temporal、DolphinScheduler)的核心模型。

DAG 编排的核心思想包括:

  • 节点(Node):代表一个可执行的任务单元,可以是原子操作或子流程。
  • 边(Edge):代表依赖方向,从前置任务指向后置任务。
  • 入度(In-degree):指向某节点的边的数量,表示该任务有多少个前置依赖。
  • 拓扑排序(Topological Sort):将 DAG 节点线性化,保证所有依赖关系被满足。
DAG 与环的约束:环的存在会导致死锁——任务 A 等待 B,B 等待 C,C 又等待 A,没有任何任务可以开始执行。所有 DAG 编排引擎在提交时都会进行环检测,拒绝包含环的图。

下图展示了一个典型的 DAG 编排结构,包含并行分支和汇合点:

DAG 任务编排示例 DAG 任务编排示例 任务 A(根) 任务 B 任务 C 任务 D 任务 E(汇合) 任务 F(终) 拓扑排序执行顺序 A → (B, C, D 并行) → E → F 入度:A(0), B(1), C(1), D(1), E(3), F(1)

2.4 三种编排模式的对比与选择

在实际开发中,一个复杂的异步流程往往混合使用多种编排模式。下表从多个维度对比三种核心模式:

维度 串行编排 并行编排 DAG 编排
依赖表达能力 仅支持线性链 仅支持无依赖 任意有向无环依赖
执行效率 低(顺序执行) 高(完全并发) 最优(按拓扑并行)
实现复杂度 低 中 高
错误定位难度 低(线性回溯) 中(需区分分支) 中高(需结合拓扑)
链路追踪支持 天然支持 需 Span 关联 需 DAG 上下文传播
典型框架 CompletableFuture.thenApply CompletableFuture.allOf Airflow / Temporal / DolphinScheduler
常见误区:不要将所有任务都设计成并行。过度并行会导致资源耗尽、上下文切换开销剧增,反而降低整体吞吐。正确的做法是:先识别依赖关系,再对无依赖的任务进行并行,最后用 DAG 统一管理拓扑。

2.5 代码示例:三种编排的 Java 实现对比

以下代码使用 Java CompletableFuture 展示三种编排模式的实现差异:

// 串行编排:任务依次执行
CompletableFuture.supplyAsync(() -> "步骤1")
    .thenApply(result -> result + " → 步骤2")
    .thenApply(result -> result + " → 步骤3")
    .thenAccept(System.out::println);

// 并行编排:任务同时执行,等待所有完成
CompletableFuture<String> taskB = CompletableFuture.supplyAsync(() -> "任务B");
CompletableFuture<String> taskC = CompletableFuture.supplyAsync(() -> "任务C");
CompletableFuture<String> taskD = CompletableFuture.supplyAsync(() -> "任务D");

CompletableFuture.allOf(taskB, taskC, taskD)
    .thenRun(() -> System.out.println("所有并行任务完成"));

// DAG 编排:先执行根任务,再并行执行三个子任务,最后汇合
CompletableFuture<String> taskA = CompletableFuture.supplyAsync(() -> "根任务");

CompletableFuture<String> taskB = taskA.thenApplyAsync(a -> a + " → B");
CompletableFuture<String> taskC = taskA.thenApplyAsync(a -> a + " → C");
CompletableFuture<String> taskD = taskA.thenApplyAsync(a -> a + " → D");

CompletableFuture<Void> taskE = CompletableFuture.allOf(taskB, taskC, taskD)
    .thenRun(() -> System.out.println("汇合点 E:所有分支完成"));

CompletableFuture<String> taskF = taskE.thenApplyAsync(v -> "最终任务 F 执行");
taskF.thenAccept(System.out::println);

从代码可以看出:串行编排通过链式调用实现;并行编排通过 allOf 聚合;DAG 编排则通过组合 thenApplyAsync 和 allOf 来表达任意拓扑。在实际生产系统中,DAG 编排通常由专门的框架管理,避免手动维护复杂的 Future 链。