第三章:规则引擎架构设计
好,咱们今天聊聊规则引擎的架构设计。说实话,这块内容我琢磨了好几年,踩过不少坑,才慢慢理清思路。你想想看,一个实时风控系统,每天要处理几百万甚至上千万的请求,规则引擎就是它的心脏。心脏跳得快不快、稳不稳,全看架构设计得怎么样。
整体架构分层:三层模型
我个人习惯把规则引擎拆成三层:规则解析层、规则执行层、结果输出层。为什么这么分?说白了,就是让每一层各司其职,互不干扰。我在项目中遇到过,有人把所有逻辑揉在一起,结果改一个规则,整个系统都要重启,那叫一个痛苦。
这三层的关系,我画了张图,你一看就明白:
嗯,这张图就是咱们这章的核心骨架。下面我逐层给你拆开讲。
规则解析层:把文本变成机器能懂的东西
规则解析层,说白了就是做翻译。把业务人员写的规则文本,翻译成机器能执行的指令。你想想看,业务人员写的是「如果交易金额大于5000且用户等级为VIP,则触发人工审核」,但机器只认0和1。这中间就需要解析层来搭桥。
解析层主要干这几件事:
- 规则加载:从数据库、文件或配置中心拉取规则。我建议用配置中心,方便热更新。
- 语法解析:把规则文本拆成一个个语法单元。比如「大于」「且」「等于」这些关键词。
- AST构建:生成抽象语法树。这棵树就是规则的逻辑骨架。
- 语义校验:检查规则有没有逻辑错误。比如「金额大于0且小于0」这种矛盾条件。
核心要点:解析层要支持多种规则格式。我在项目中遇到过,有的团队用JSON,有的用YAML,还有的直接写DSL。所以解析层最好设计成插件式,每种格式一个解析器。
举个例子,一条简单的规则解析后长这样:
// 原始规则文本
IF amount > 5000 AND user_level == 'VIP' THEN action = 'manual_review'
// 解析后的AST结构
{
"type": "Rule",
"condition": {
"type": "AND",
"left": {
"type": "COMPARISON",
"operator": ">",
"field": "amount",
"value": 5000
},
"right": {
"type": "COMPARISON",
"operator": "==",
"field": "user_level",
"value": "VIP"
}
},
"action": {
"type": "ASSIGN",
"field": "action",
"value": "manual_review"
}
}
小技巧:解析层可以加一层缓存。规则一般不会频繁变更,把解析好的AST缓存起来,能省不少CPU。我习惯用LRU缓存,设置个1000条上限就够了。
规则执行层:真正干活的地方
执行层是引擎的核心。解析层把规则变成了AST,执行层就要拿着AST去匹配数据。这里有个关键点:执行效率。实时风控对延迟要求极高,通常要求毫秒级响应。
执行层的工作流程:
- 接收事件:拿到一笔交易或一次登录的上下文数据。
- 条件匹配:遍历所有规则,看哪些规则的条件被满足。
- 动作执行:对匹配的规则,执行对应的动作。比如拒绝交易、发送验证码。
- 优先级调度:规则有优先级,高优先级的先执行。我建议用优先级队列来管理。
这里有个坑,我曾经踩过:规则冲突。两条规则同时匹配,一条说「通过」,一条说「拒绝」,听谁的?我的做法是引入冲突解决策略:
| 策略名称 | 说明 | 适用场景 |
|---|---|---|
| 优先级优先 | 优先级高的规则胜出 | 大部分场景 |
| 拒绝优先 | 只要有一条规则拒绝,就拒绝 | 高风险场景 |
| 投票机制 | 多数规则的意见为准 | 复杂决策场景 |
注意:执行层一定要做超时控制。我曾经遇到过,一条规则里有个远程调用,结果对方服务挂了,整个引擎卡死。后来我加了超时熔断,超过50毫秒直接跳过这条规则。
执行层的代码骨架大概长这样:
public class RuleEngine {
private List<Rule> rules; // 已解析的规则列表
public ExecutionResult execute(EventContext context) {
// 1. 按优先级排序
rules.sort((a, b) -> b.priority - a.priority);
// 2. 遍历匹配
for (Rule rule : rules) {
if (timeoutExceeded()) break; // 超时保护
if (rule.matches(context)) {
Action action = rule.getAction();
action.execute(context);
// 3. 如果动作是终止型的,停止后续规则
if (action.isTerminal()) break;
}
}
// 4. 返回结果
return context.getResult();
}
}
结果输出层:把决策送出去
结果输出层,听起来简单,其实门道不少。执行层算出了结果,怎么送出去?送给谁?要不要存下来?这些都是问题。
输出层主要做三件事:
- 结果聚合:把多条规则的执行结果汇总。比如三条规则都匹配了,但动作不同,需要合并成一个最终决策。
- 风险评级:给结果打个分。0到100,越高越危险。我习惯用加权平均,每条规则贡献一个分数。
- 处置动作:执行具体的处置。比如调用风控系统的API、发送告警短信。
输出层还有一个容易被忽略的功能:异步回调。有些动作不需要同步返回,比如事后分析、数据归档。我建议把这类操作丢到消息队列里,异步处理,别阻塞主流程。
关键设计:输出层要支持多种输出方式。我在项目中遇到过,有的业务方要HTTP回调,有的要写Kafka,还有的要存数据库。所以输出层最好设计成输出通道模式,每种通道一个实现类,通过配置动态加载。
输出层的配置示例:
output:
channels:
- type: http
url: http://risk-service/api/decision
timeout: 100ms
retry: 2
- type: kafka
topic: risk-decision
partition: 0
- type: database
table: decision_log
ttl: 30d
嗯,三层架构讲完了。你发现没有,每一层都有自己明确的职责,层与层之间通过接口通信。这样设计的好处是:可以单独升级、替换某一层。比如你想把解析层从JSON换成Protobuf,只要接口不变,执行层和输出层完全不用动。
最后说一句,架构设计没有银弹。我这套三层模型,在大部分场景下够用。但如果你遇到极端场景——比如每秒10万笔交易——那可能还需要加一层预过滤层,把明显安全的请求直接放行。这个咱们后面再聊。
公众号:蓝海资料掘金营,微信deep3321