规则库别写成面条代码:模块化拆分与多环境配置实战
去年接手一个风控规则模块,第一眼看过去全是 if-else 嵌套,环境差异靠硬编码 switch(env) 兜底,改一条规则要发版三次。重构时我们只盯住两件事:怎么拆,怎么配。
先给结论:规则库不该是单一巨类。按职责切四块最稳:解析层(Parser)、校验层(Validator)、执行层(Executor)、注册层(Registry)。别混在一起。
解析层只负责把文本变成可计算的结构。别一上来就手写递归下降,ANTLR 或 Lark 足够应付 90% 的场景。如果规则简单到只有 && || > < ==,直接上 AST 树就行。
校验层必须前置。很多团队把语法错误拖到线上才暴露。在 CI 阶段跑一遍静态检查,结合 JSON Schema 或自定义的 AST Visitor,拦截非法字段、死循环引用、越权函数调用。校验失败直接阻断构建,别留到运行时。
执行层做纯计算。输入上下文(Context),输出结果(Result)。这里建议用 DAG(有向无环图)编排执行顺序,避免隐式依赖。每个节点只暴露 evaluate(ctx) 接口,方便单元测试和 Mock。
注册层管缓存和版本。规则变更频率高,别每次请求都读磁盘或查库。本地内存 LRU + 版本号比对是标配。支持热加载时,务必做双缓冲切换,旧版本处理完再释放,不然线上会出现半截规则报错。
DSL 还是 JSON/YAML?
直接上 YAML 存业务逻辑是灾难。YAML 擅长描述结构,不擅长表达逻辑。我们踩过的坑:运营在 YAML 里写了一段三元嵌套,缩进错一位,整个活动页瘫痪。
更稳妥的做法是 元数据用 YAML,逻辑用内嵌 DSL。
rule_id: RISK_LOGIN_003
version: "1.2"
priority: 80
env_overrides: {} # 环境差异放这里,下面细说
context_fields: [user_level, login_count_24h, ip_risk_score]
logic: |
if user_level == 'VIP' && login_count_24h > 10:
return BLOCK
else if ip_risk_score > 0.7:
return VERIFY_SMS
else:
return PASS
YAML 负责声明元信息、依赖字段、生效范围。logic 字段塞一段轻量 DSL(SpEL、MVEL 或自研子集)。执行层拿到后,由解析层转成 AST,校验层跑类型检查,最后编译成字节码或解释执行。这样运营能看懂结构,研发能控制逻辑边界。
多环境差异怎么管?
别写 if env == 'staging'。用 基础配置 + 增量 Patch 的模式。
- 维护一份
base.yaml,放全环境通用的规则和默认阈值。 - 每个环境建
patch-dev.yaml、patch-prod.yaml,只写差异项。比如 prod 里把ip_risk_score阈值从0.7提到0.85。 - 构建或部署阶段,用工具(类似 Kustomize 或自研合并脚本)按优先级叠加。叠加顺序必须严格固定:
base -> dev/staging -> prod。 - 合并后生成一份完整的、带哈希签名的
merged.yaml,随包发布。运行时只读这份最终文件,环境隔离彻底完成。
CI 流水线里加一步 Dry-Run:拿测试集跑一遍合并后的规则,对比预期输出。通过率不到 100% 直接打回。线上回滚也简单,切回上一个版本的 merged.yaml 即可,不需要改代码。
几个容易翻车的细节
- 版本漂移:规则文件和代码强绑定。规则库必须带
schema_version和rule_version,不兼容时拒绝加载。 - 热更新雪崩:别用
FileWatch轮询触发全量重载。用配置中心推送版本号,节点按需拉取,加防抖锁。 - 权限越界:DSL 里别开放
eval()、os.system()或反射调用。白名单限定可用函数,超范围直接抛安全异常。 - 审计缺失:规则命中结果必须落日志,带
rule_id、version、input_hash、output。出事时能精准回溯是哪条规则、哪个版本拦住了请求。
规则库不是越复杂越好。先把解析、校验、执行、注册四条线理清,用 YAML 管结构,DSL 管逻辑,Patch 管环境。跑通闭环后,再考虑可视化编排或动态编译。步子别迈太大,线上稳定永远是第一位。