4. 为什么使用DAG
在风控系统中,规则很少是孤立运行的。一条“高风险设备登录”规则可能依赖“设备指纹识别”的结果,而“设备指纹识别”又依赖“用户历史行为画像”的更新。如果把这些规则简单塞进一个列表里顺序执行,系统会变得又慢又脆:改一条规则可能影响十条,排查问题像大海捞针,更严重的是,当规则之间出现循环依赖时,执行引擎可能陷入死循环或产生不可预期的结果。DAG(有向无环图)正是解决这类问题的核心建模工具。
一、核心原理:DAG如何表达规则依赖
DAG由“节点”和“有向边”组成。在风控规则场景中,节点代表一条规则、一个特征计算任务或一个决策动作;有向边代表依赖关系,方向从“被依赖方”指向“依赖方”。例如规则B需要规则A的输出,就画一条A→B的边。“无环”是DAG的灵魂:从任意节点出发,沿着边一直走,永远不可能回到起点。这个约束直接排除了循环依赖,保证了执行顺序一定存在。
相比线性列表或普通图,DAG的建模优势体现在三个方面:
- 依赖显式化:每条规则的前置条件变成图上的边,而不是散落在代码里的if判断或隐式调用顺序。
- 变更可影响分析:修改一条规则时,沿出边方向即可找到所有下游受影响规则,快速评估影响范围。
- 执行可优化:无依赖关系的规则可以并行执行,依赖关系明确的规则可以按拓扑顺序串行执行,兼顾正确性与性能。
一句话理解:DAG把“谁先谁后”从程序员的记忆和代码注释中解放出来,变成引擎可以自动计算和校验的结构化数据。
二、方法流程:拓扑排序与并行执行
有了DAG,执行引擎需要解决两个问题:第一,按什么顺序执行才能保证依赖满足?第二,哪些规则可以同时执行?答案分别是拓扑排序和分层并行。
拓扑排序的基本思路是:每次选出所有入度为0的节点(即没有未满足前置依赖的规则),将它们放入当前执行批次;执行完成后,移除这些节点及其出边,更新下游节点的入度;重复直到所有节点被处理。如果最终仍有节点入度不为0,说明图中存在环,必须拒绝执行并报警。
输入:规则依赖图 G = (V, E)
输出:执行批次列表 batches
1. 计算每个节点的入度 indegree
2. 将 indegree == 0 的节点加入队列 queue
3. while queue 非空:
4. current_batch = queue 中所有节点
5. batches.append(current_batch)
6. for node in current_batch:
7. for downstream in node 的出边邻居:
8. indegree[downstream] -= 1
9. if indegree[downstream] == 0:
10. queue.append(downstream)
11. if 已处理节点数 < 总节点数:
12. raise CycleDetectedError
13. return batches
这个算法天然支持并行:同一批次内的规则互不依赖,可以交给线程池、协程或分布式任务队列同时执行。批次之间则形成天然的屏障,上一批全部完成后才进入下一批。对于风控场景,这意味着“设备指纹”“IP画像”“历史订单统计”等独立特征可以并行计算,而“综合风险评分”必须等它们全部完成后才能执行。
下面这张图展示了一个典型的风控规则DAG及其分层执行结果:
三、案例/场景:冲突避免与执行优化
假设某支付风控系统有以下规则:R1计算设备指纹,R2查询IP归属地,R3统计用户近1小时交易次数,R4根据R1和R2计算设备风险分,R5根据R3和用户画像计算行为异常分,R6综合R4和R5输出最终决策。用DAG建模后,拓扑排序得到三个批次:批次1执行R1、R2、R3;批次2执行R4、R5;批次3执行R6。原本串行需要6个时间单位,并行后只需3个时间单位,性能提升一倍。
更重要的是冲突避免。在风控规则中,冲突常表现为:两条规则同时修改同一个风险标签,或一条规则读取了另一条规则尚未写入的特征。DAG通过依赖边强制规定写入先于读取,从结构上消除了这类竞态条件。例如R4必须等R1和R2都完成后才能读取设备指纹和IP归属地,引擎不会在R1未完成时调度R4。
| 对比维度 | 线性规则列表 | DAG依赖图 |
|---|---|---|
| 依赖表达 | 隐式,靠代码顺序和注释 | 显式,边即依赖 |
| 循环检测 | 难以发现,运行时可能死循环 | 拓扑排序时直接报错 |
| 并行能力 | 几乎无,只能顺序执行 | 同层节点自动并行 |
| 变更影响分析 | 人工排查,易遗漏 | 沿出边遍历即可 |
| 冲突避免 | 依赖运行时锁或人工约定 | 结构上保证读写顺序 |
四、误区/复盘:DAG不是银弹
常见误区一:把DAG当成万能流程图。DAG只表达依赖关系,不表达条件分支、循环和异常回滚。风控中“如果设备风险分大于80则直接拒绝”这类条件逻辑,应放在节点内部或单独的决策节点中,而不是试图用DAG的边来表达。
常见误区二:忽视动态依赖。有些规则的依赖关系在运行时才能确定,例如“仅当用户为高风险等级时才执行人工审核规则”。如果强行把所有可能依赖都画成静态边,会导致大量不必要的等待,降低并行度。实践中可以用条件边或子图动态展开来解决。
常见误区三:拓扑排序只做一次。规则图会随业务迭代变化,新增规则、修改依赖后必须重新校验无环性并重新计算批次。建议把环检测纳入规则发布流水线,任何导致成环的变更直接拒绝上线。
复盘建议:在风控规则DAG落地时,优先保证“无环校验”和“批次日志”两项能力。无环校验防止系统崩溃,批次日志记录每批实际执行了哪些规则、耗时多少,是后续优化并行度和排查依赖错误的第一手依据。
总结来说,DAG为风控规则提供了一种可校验、可优化、可演进的依赖建模方式。拓扑排序回答了“按什么顺序执行”,分层并行回答了“哪些能同时执行”,而结构化的依赖边则从根源上避免了读写冲突和循环依赖。掌握这三者的配合,是构建高性能、高可靠风控规则引擎的关键一步。