目录
[DAG 基本概念](#DAG 基本概念)
[DAG 在工作流中的核心作用 :](#DAG 在工作流中的核心作用 :)
[目的 :](#目的 :)
[二、DFS 检测循环](#二、DFS 检测循环)
[三、DAG 经过拓扑排序后,按照依赖顺序依次执行每个节点,将前一个节点的输出作为下一个节点的输入,并记录每个节点的执行结果](#三、DAG 经过拓扑排序后,按照依赖顺序依次执行每个节点,将前一个节点的输出作为下一个节点的输入,并记录每个节点的执行结果)
DAG 基本概念
DAG是一种特殊的有向图,具有以下特点:
-
有向性 :边有方向,表示依赖关系
-
无环性 :图中不存在任何循环路径
-
拓扑结构 :节点可以按依赖关系排序
DAG 在工作流中的核心作用 :
-
结构化表达 :用图形化方式表达任务依赖
-
智能排序 :自动计算正确的执行顺序
-
安全保障 :检测并防止循环依赖
-
效率提升 :支持并行执行
适用场景:
DAG 必须在运行前"画好图",无法应对"运行时动态生成任务"的场景。
DAG 模型最适合那些"输入确定、逻辑固定、流向单一"的系统。
缺陷:
DAG 模型在处理**"**反馈回路、运行时变更、复杂状态流转"时存在天然的数学和逻辑缺陷。
1、传统 DAG 引擎需要在运行前就确定好图的结构(静态定义)。虽然现代引擎引入了动态任务映射,但本质上还是在静态框架下"打补丁",调试和维护都非常痛苦。
2、DAG 严禁环路。要实现"重试"或"循环",你必须把逻辑封装在一个黑盒任务内部,或者使用极其复杂的"触发新 DAG"的外部机制,导致流程割裂。
一、拓扑排序
目的 :
将有向无环图(DAG)中的节点按照依赖关系排列成线性顺序 ,确保每个节点只有在其所有前置依赖节点都出现之后才会出现。常用算法 :Kahn 算法 :基于入度的广度优先算法
步骤:
从没有依赖的节点开始,逐步处理,每处理一个节点就减少其后继节点的依赖计数,直到所有节点都被处理 。
实现:
java
/**
* 拓扑排序(Kahn 算法)
*/
(1)nodeMap:key为节点Id,值为工作流节点模型
(2)dependencies:key为节点Id,值为该节点的前置节点ID集合
private List<WorkflowNode> topologicalSort(Map<String, WorkflowNode> nodeMap,
Map<String, List<String>> dependencies) {
List<WorkflowNode> result = new ArrayList<>();
// 计算每个节点的入度
Map<String, Integer> inDegree = new HashMap<>();
for (String nodeId : nodeMap.keySet()) {
inDegree.put(nodeId, dependencies.get(nodeId).size());
}
// 找出所有入度为 0 的节点(起始节点)
Queue<String> queue = new LinkedList<>();
for (Map.Entry<String, Integer> entry : inDegree.entrySet()) {
if (entry.getValue() == 0) {
queue.offer(entry.getKey());
}
}
// 构建反向依赖图(用于拓扑排序)
Map<String, List<String>> dependents = new HashMap<>();
for (String nodeId : nodeMap.keySet()) {
dependents.put(nodeId, new ArrayList<>());
}
for (Map.Entry<String, List<String>> entry : dependencies.entrySet()) {
String target = entry.getKey();
for (String source : entry.getValue()) {
dependents.get(source).add(target);
}
}
// Kahn 算法执行拓扑排序
while (!queue.isEmpty()) {
String nodeId = queue.poll();
result.add(nodeMap.get(nodeId));
// 将该节点的所有后继节点的入度减 1
List<String> deps = dependents.get(nodeId);
if (deps != null) {
for (String dep : deps) {
int degree = inDegree.get(dep) - 1;
inDegree.put(dep, degree);
if (degree == 0) {
queue.offer(dep);
}
}
}
}
// 如果排序后的节点数小于总节点数,说明存在循环依赖
if (result.size() != nodeMap.size()) {
throw new RuntimeException("工作流存在循环依赖,无法完成拓扑排序");
}
return result;
}
二、DFS 检测循环
作用:
在项目中,DFS 检测是工作流执行前的 安全检查 ,确保工作流没有循环依赖,避免无限执行。
步骤:
-
递归遍历 :深度优先搜索每个节点
-
路径记录 :用 currentPath 记录当前路径
-
循环判断 :如果当前节点在 currentPath 中,说明存在循环
-
回溯清理 :递归返回时从 currentPath 中移除节点
实现:
java
/**
* 检测循环依赖(使用 DFS)
*/
private void detectCycle(Map<String, List<String>> dependencies, List<WorkflowNode> nodes) {
Set<String> visited = new HashSet<>();
Set<String> currentPath = new HashSet<>();
for (WorkflowNode node : nodes) {
if (hasCycleDFS(node.getId(), dependencies, visited, currentPath)) {
throw new RuntimeException("工作流存在循环依赖,节点: " + node.getId());
}
}
}
/**
* DFS 检测循环
*/
private boolean hasCycleDFS(String nodeId, Map<String, List<String>> dependencies,
Set<String> visited, Set<String> currentPath) {
if (currentPath.contains(nodeId)) {
return true; // 有循环
}
if (visited.contains(nodeId)) {
return false; // 已访问
}
visited.add(nodeId);
currentPath.add(nodeId);
// 递归检查所有依赖节点
List<String> deps = dependencies.get(nodeId);
if (deps != null) {
for (String dep : deps) {
if (hasCycleDFS(dep, dependencies, visited,currentPath)) {
return true;
}
}
}
currentPath.remove(nodeId);
return false;
}
三、DAG 经过拓扑排序后,按照依赖顺序依次执行每个节点,将前一个节点的输出作为下一个节点的输入,并记录每个节点的执行结果
java
List<ExecutionResponse.NodeResult> nodeResults = new ArrayList<>();//存储执行结果
List<WorkflowNode> sortedNodes;//拓扑排序后的节点列表
for(WorkflowNode node:sortedNodes){
NodeExecutor executor = executorFactory.getExecutor(node.getType());//使用节点执行器执行
Map<String, Object> output = executor.execute(node, currentInput);
//封装执行结果
ExecutionResponse.NodeResult nodeResult = new ExecutionResponse.NodeResult();
nodeResult.setNodeId(node.getId());
nodeResult.setNodeName(node.getType());
nodeResult.setInput(JSON.toJSONString(currentInput));
nodeResult.setStatus("SUCCESS");
nodeResult.setOutput(JSON.toJSONString(output));
nodeResults.add(nodeResult);
//当前输出为下一个节点的输入
currentInput = output;
}
可并行扩展:
DAG 的精髓在于并行: 如果节点 A 和节点 B 都没有依赖,它们应该同时跑。目前的 Kahn 算法逻辑里,入度为 0 的节点可能有多个,可以用线程池或协程来同时处理这些节点,而不是用一个 for 循环挨个跑。
java
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class DagParallelRunner {
// 1. 线程池 (生产环境建议自定义配置核心线程数)
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
// 2. 全局上下文线程安全
private final Map<String, Object> globalContext = new ConcurrentHashMap<>();
/**
* 并行执行入口
* @param nodes 所有节点列表
* @param dependencies 依赖关系 Map<当前节点ID, 前置节点ID列表>
*/
public void execute(List<WorkflowNode> nodes, Map<String, List<String>> dependencies) throws InterruptedException {
// 3. 构建反向依赖图 (用于任务完成后通知下游)
Map<String, List<String>> reverseDeps = new HashMap<>();
for (WorkflowNode node : nodes) reverseDeps.put(node.getId(), new ArrayList<>());
for (Map.Entry<String, List<String>> entry : dependencies.entrySet()) {
String target = entry.getKey();
for (String source : entry.getValue()) {
reverseDeps.get(source).add(target);
}
}
// 4. 计算每个节点的初始入度 (运行时动态扣减)
Map<String, AtomicInteger> inDegreeMap = new ConcurrentHashMap<>();
for (WorkflowNode node : nodes) {
int inDegree = dependencies.get(node.getId()).size();
inDegreeMap.put(node.getId(), new AtomicInteger(inDegree));
}
// 5. 找出所有入度为0的起始节点,直接提交执行
Queue<String> readyQueue = new ConcurrentLinkedQueue<>();
for (WorkflowNode node : nodes) {
if (inDegreeMap.get(node.getId()).get() == 0) {
readyQueue.offer(node.getId());
}
}
// 6. 核心并发循环
// 使用 CountDownLatch 等待所有任务结束
CountDownLatch latch = new CountDownLatch(nodes.size());
while (true) {
String nodeId = readyQueue.poll();
if (nodeId == null) break; // 队列为空,说明所有可执行任务已提交
WorkflowNode node = nodes.stream().filter(n -> n.getId().equals(nodeId)).findFirst().orElse(null);
if (node == null) continue;
// 提交任务到线程池
executorService.submit(() -> {
try {
// --- 执行业务逻辑 ---
NodeExecutor executor = new ExecutorFactory().getExecutor(node.getType()); // 假设工厂类已实现
// 从全局上下文收集输入 (这里简单传入所有,实际可按需过滤)
Map<String, Object> input = new HashMap<>(globalContext);
Map<String, Object> output = executor.execute(node, input);
// 结果写入全局上下文
globalContext.put(node.getId(), output);
System.out.println("节点 " + node.getId() + " 执行完成");
} catch (Exception e) {
e.printStackTrace();
// 生产环境需处理失败逻辑(如阻断下游)
} finally {
// --- 调度逻辑:通知下游节点 ---
List<String> successors = reverseDeps.get(node.getId());
if (successors != null) {
for (String successorId : successors) {
// 入度减 1,如果变为 0 则加入就绪队列
if (inDegreeMap.get(successorId).decrementAndGet() == 0) {
readyQueue.offer(successorId);
}
}
}
latch.countDown();
}
});
}
// 7. 等待所有任务执行完毕
latch.await();
System.out.println("DAG 全部执行完毕!");
executorService.shutdown();
}
}
---------------------------------------------------------分割线---------------------------------------------------------------
本篇文章到此结束,感谢阅读!
完结撒花 ✿✿✿✿✿✿