3. 数据质量规则引擎架构设计:整体架构分层与核心模块
聊到规则引擎的架构,我习惯先画一张分层图。你想想看,一个规则跑起来,从数据进来,到规则匹配,再到结果输出,中间要经过好几个环节。如果全揉在一起,后期维护就是灾难。
我在项目中遇到过好几次这样的场景:业务方说“规则不生效”,结果一查,是数据源层和规则执行层的逻辑耦合太紧,改一处牵动全身。所以,分层这件事,不是理论上的花架子,是实实在在的避坑手段。
3.1 整体架构分层
我把整个引擎分成四层,从上往下看:
- 数据源层:负责对接各种数据来源。比如关系库、Hive、Kafka 消息、甚至是 Excel 文件。
- 规则定义层:用户在这里配置规则。包括规则的条件、阈值、权重、生效范围等。
- 规则执行层:引擎的核心。它拿到规则定义,去数据源层拉数据,然后执行校验逻辑。
- 结果输出层:把执行结果包装成标准格式,推给下游。比如告警系统、数据血缘平台、或者直接写回表。
嗯,这里要注意:数据源层和规则执行层之间,一定要有数据抽象。我曾经见过一个项目,规则执行层直接写 JDBC 连接数据库,结果数据库一换,规则全得重写。说白了,数据源层应该把数据封装成统一的“数据记录”对象,执行层只管处理这个对象就行。
3.2 核心模块设计
四层架构是骨架,核心模块才是血肉。我重点讲三个模块:规则解析器、规则调度器、结果评估器。这三个模块都在规则执行层里,但它们各司其职。
3.2.1 规则解析器
规则解析器干的事,就是把人类可读的规则配置,转成机器可执行的逻辑。比如用户配了一条规则:“订单金额不能为负数”,解析器要把它转成一个表达式对象。
我个人习惯用 AST(抽象语法树) 来做这件事。为什么?因为 AST 可以支持复杂的嵌套条件。举个例子:
// 原始规则配置(JSON 格式)
{
"ruleId": "R001",
"field": "order_amount",
"operator": "GREATER_THAN_OR_EQUAL",
"value": 0,
"severity": "ERROR"
}
// 解析后生成的表达式对象(伪代码)
Expression expr = new ComparisonExpression(
new FieldReference("order_amount"),
Operator.GTE,
new LiteralValue(0)
);
我曾经踩过一个坑:直接用字符串拼接的方式生成 SQL 来执行规则。结果遇到规则条件里有特殊字符,直接报错。后来改成 AST 解析,把规则和底层存储彻底解耦,再也没出过类似问题。
3.2.2 规则调度器
规则调度器负责两件事:什么时候跑 和 怎么并发跑。
什么时候跑?常见的有三种触发方式:
| 触发方式 | 适用场景 | 我遇到的坑 |
|---|---|---|
| 定时调度 | 离线数仓的日/周质量巡检 | 高峰期调度任务扎堆,导致资源争抢 |
| 事件触发 | 实时数据流接入时即时校验 | 事件量暴增时来不及处理,需要背压机制 |
| 手动触发 | 数据问题排查时临时跑一次 | 权限管控不到位,有人乱跑全量规则 |
怎么并发跑?我建议用 规则组 + 依赖拓扑 的方式。比如规则 A 和规则 B 没有依赖关系,可以并行执行;规则 C 依赖规则 A 的结果,那就等 A 跑完再跑 C。调度器内部维护一张 DAG(有向无环图),按拓扑排序来调度。
// 调度器核心逻辑(伪代码)
Dag dag = buildDag(rules); // 根据规则依赖构建 DAG
List<RuleNode> sorted = dag.topologicalSort();
for (RuleNode node : sorted) {
if (node.getDependencies().isEmpty()) {
// 无依赖,直接提交线程池
executor.submit(() -> executeRule(node));
} else {
// 有依赖,等前置规则完成
waitForDependencies(node);
executor.submit(() -> executeRule(node));
}
}
3.2.3 结果评估器
结果评估器是最后一道关卡。它拿到规则执行后的原始结果(比如“字段 A 有 100 条空值”),然后做三件事:
- 阈值判定:空值率是否超过 5%?超过就标记为“不通过”。
- 严重等级赋值:根据规则配置的 severity,给结果打上 ERROR、WARN、INFO 等标签。
- 结果聚合:把多条规则的结果合并成一份质量报告。比如“表 T1 共有 10 条规则,通过 8 条,失败 2 条”。
我习惯在结果评估器里加一个 评分模型。不光是简单的“通过/不通过”,而是算出一个综合质量分数。比如:
// 结果评估示例(伪代码)
double score = 100.0;
for (RuleResult result : allResults) {
if (result.getStatus() == Status.FAILED) {
// 根据严重等级扣分
switch (result.getSeverity()) {
case ERROR: score -= 20; break;
case WARN: score -= 10; break;
case INFO: score -= 2; break;
}
}
}
score = Math.max(0, score); // 最低 0 分
这个分数可以直观地告诉业务方:“这张表的质量分是 85 分,主要失分点在订单金额字段的空值率过高。” 比单纯说“规则不通过”有用得多。