第三章:规则引擎架构设计

好,咱们今天聊聊规则引擎的架构设计。说实话,这块内容我琢磨了好几年,踩过不少坑,才慢慢理清思路。你想想看,一个实时风控系统,每天要处理几百万甚至上千万的请求,规则引擎就是它的心脏。心脏跳得快不快、稳不稳,全看架构设计得怎么样。

整体架构分层:三层模型

我个人习惯把规则引擎拆成三层:规则解析层规则执行层结果输出层。为什么这么分?说白了,就是让每一层各司其职,互不干扰。我在项目中遇到过,有人把所有逻辑揉在一起,结果改一个规则,整个系统都要重启,那叫一个痛苦。

这三层的关系,我画了张图,你一看就明白:

规则引擎三层架构 规则解析层 规则加载 → 语法解析 → AST构建 → 语义校验 规则执行层 条件匹配 → 动作执行 → 上下文管理 → 优先级调度 结果输出层 结果聚合 → 风险评级 → 处置动作 → 异步回调 数据流向

嗯,这张图就是咱们这章的核心骨架。下面我逐层给你拆开讲。

规则解析层:把文本变成机器能懂的东西

规则解析层,说白了就是做翻译。把业务人员写的规则文本,翻译成机器能执行的指令。你想想看,业务人员写的是「如果交易金额大于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去匹配数据。这里有个关键点:执行效率。实时风控对延迟要求极高,通常要求毫秒级响应。

执行层的工作流程:

  1. 接收事件:拿到一笔交易或一次登录的上下文数据。
  2. 条件匹配:遍历所有规则,看哪些规则的条件被满足。
  3. 动作执行:对匹配的规则,执行对应的动作。比如拒绝交易、发送验证码。
  4. 优先级调度:规则有优先级,高优先级的先执行。我建议用优先级队列来管理。

这里有个坑,我曾经踩过:规则冲突。两条规则同时匹配,一条说「通过」,一条说「拒绝」,听谁的?我的做法是引入冲突解决策略

策略名称 说明 适用场景
优先级优先 优先级高的规则胜出 大部分场景
拒绝优先 只要有一条规则拒绝,就拒绝 高风险场景
投票机制 多数规则的意见为准 复杂决策场景

注意:执行层一定要做超时控制。我曾经遇到过,一条规则里有个远程调用,结果对方服务挂了,整个引擎卡死。后来我加了超时熔断,超过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