
1. 这次调研的起因业务规则已经从配置变成了代码债1.1 每次改规则都要发版问题不在发版本身前一阵子业务方提了个听起来很简单的需求把订单中心里“新客立减”的优惠门槛从满 100 改为满 99生效范围限定在指定渠道并且要求这次改动能像配置一样上线而不是等我们排版本。就是这个需求逼着我做了一轮完整的规则引擎调研分析。当时我们订单中心的优惠计算逻辑已经有几千行结构上做了策略模式、责任链甚至加了一部分 SPI 扩展。单看某一个策略类代码也算清晰。但把所有规则组合在一起之后问题就暴露了满减规则依赖用户等级会员折扣依赖商品类目新客立减又和渠道身份绑定。规则之间存在隐式优先级改动一条往往要连带评估另外两三条。业务方想要的很简单就是改一个数字。但对我们来说这个数字散落在代码里和编译、测试、发布紧紧绑在一起。更难受的是规则只能由开发维护。运营每次想调整活动哪怕只是改个金额阈值都要提工单、排期、上线。时间一长开发同学变成了“改规则的机器人”业务方又觉得响应太慢。规则已经从最初的业务配置慢慢变成了一笔沉重的代码债。1.2 调研前我圈定的三个问题做规则引擎调研很容易一头扎进产品对比里但我在开始之前先给自己定了三个问题后面所有选型判断都围绕这三个问题展开。第一规则能不能真正和业务代码解耦。我希望实现的效果是“条件”和“动作”可以被描述成数据存在数据库或配置文件里而不是散落在 Java 方法中。业务方只需要通过后台修改参数不需要开发改代码。这个目标听起来简单实际做起来会发现很多规则引擎只解决了“怎么执行规则”没解决“规则从哪来、谁来改、怎么审批”。第二引入规则引擎之后系统性能和稳定性会不会变差。订单优惠计算处在核心链路上哪怕多消耗 5 毫秒在大促流量下都可能被放大。所以我不光要看引擎的执行速度还要看它能否缓存编译结果、是否支持热加载、规则数量增加后性能是否线性劣化。第三团队的学习成本和维护成本是否可控。规则引擎本身也是系统组件它需要监控、需要排障、需要人维护。如果团队没人愿意学复杂的 DSL选了一个功能再强的引擎最后也只会被弃用。这三个问题在调研过程中不断帮我把方案拉回现实。1.3 这次调研的边界既然要做调研分析我需要先划定边界避免被“规则引擎”这个宽泛概念带跑。我的重点是开源 Java 生态里的规则引擎和类规则引擎方案包括 Drools、Easy Rules、Aviator、QLExpress、LiteFlow 这几个常见选项。商业业务规则管理系统BRMS不在考虑范围内因为我们需要控制基础设施成本和供应商风险。工作流引擎也不在这次调研范围内虽然它和规则引擎经常被放在一起比较但解决的问题其实是两回事。另一个边界是我没有把“可视化规则编排平台”作为必需项。很多团队一上来就追求业务人员能在界面上拖拽配置规则但实际落地中复杂的可视化平台反而会成为新的维护负担。我更倾向于先用规则文件或规则表的形式跑通流程再逐步给运营配置人力所能及的配置界面。这个取舍在后面选型时起到了关键作用。2. 规则引擎拆开看核心概念与工作方式2.1 先区分规则引擎、表达式引擎和编排引擎在调研过程中我发现一个普遍误区很多人把表达式引擎、规则引擎、流程编排引擎混为一谈。这三者的定位差异非常明显。表达式引擎的核心能力是“算”它接收一个表达式和一组变量返回计算结果。比如判断amount 100 user.isNew()或者计算折扣金额。它不关心规则之间是什么关系也不帮你管理规则优先级。规则引擎的核心能力是“判断和组织”它接收一批事实Fact把它和一组规则做匹配命中后触发对应的动作。规则引擎会处理规则之间的优先级、互斥关系和依赖关系它更像一个裁判员规则都说清楚了事实放进来裁判决定吹哪个哨。流程编排引擎的核心能力是“串联”它把一个业务链路拆成多个步骤通过 DSL 或配置控制步骤的先后顺序和分支条件。它本身不一定关心“条件是否成立”更关心“下一步该执行谁”。这三者的边界在实际项目中是模糊的很多系统用表达式引擎实现了规则引擎的效果也有系统用流程编排引擎把规则节点串起来。理解差异后再看选型就会清晰很多。2.2 一条规则的标准结构无论用哪款引擎规则的核心结构都差不多条件部分LHS和动作部分RHS再加上名称、优先级、描述等元数据。一句话概括就是“当满足什么条件执行什么动作”。以 Drools 为例一条简单的满减规则在 DRL 文件里长这样rule new_user_discount salience 10 when $order: Order(amount 100) $user: User(tags contains NEW_USER) then $order.applyDiscount(20); end如果换成 Easy Rules 这类轻量级引擎规则就变成 Java 注解和对象Rule(name new user discount, description 新客满减) public class NewUserDiscountRule { Condition public boolean isNewUser(Fact(user) User user) { return user.isNew(); } Action public void applyDiscount(Fact(order) Order order) { order.discount(20); } }无论哪种形式规则的本质都是一种“可命中的判定逻辑”。真正影响选型的是规则数量、规则之间的关系以及引擎如何处理这些关系。2.3 Rete 算法为什么规则引擎可以处理成百上千条规则Drools 之类完整规则引擎的核心竞争力在于 Rete 算法。简单理解Rete 会先扫描一遍规则集合把条件中的公共模式提取出来构建成一个匹配网络。事实进来时不是一条一条规则去扫而是把事实沿着网络传播每个节点只做自己那部分匹配。匹配结果还会被缓存当事实变化时只更新受影响的路径。用图书馆来类比普通做法是读者每找一个主题就翻一遍书架Rete 则先按分类建好索引新书进来直接放到对应分类查询时只去相关区域找。规则数量越大这种索引优势越明显。但要注意不是所有规则引擎都实现了 Rete。Easy Rules 默认是线性遍历所有规则规则量少时完全够用一旦规则到了上千条就要压测确认是否会成为瓶颈。这也解释了为什么 Drools 很重但依然有不可替代的地位。3. 几款主流规则引擎的横向对比从重到轻的选型谱系3.1 Drools最正宗也最重的规则引擎Drools 是 JBoss 社区维护的开源规则引擎采用 Rete 算法支持 DRL 规则文件、决策表、规则流等。你可以把一组规则打包进 KieContainer通过 KieBase 和 KieSession 跟业务代码交互。功能上它确实能覆盖大多数复杂规则场景包括激活组、议程组、规则流等等。但重也是真的重。Drools 的 DSL 和运行时模型比较复杂团队需要专门学习 Rete 概念、Kie 容器生命周期、冲突解决策略。规则文件一旦多了没有配套管理工具的话排查一条规则为什么没命中会变得很痛苦。它在银行、保险这种规则数量大、规则关系复杂、需要严格审核的场景下很合适但对绝大多数互联网业务团队来说可能有点高射炮打蚊子。3.2 Easy Rules适合中小团队的轻量级方案Easy Rules 是一个很务实的轻量级规则引擎。它没有外部 DSL规则直接用 Java 注解或者 RuleBuilder 定义Fact 用 Map 传递。使用起来几乎不需要额外学习成本项目里引入依赖就能用。规则之间可以通过 Rule 对象上的 compareTo 或者引擎的 RulePriorityComparator 控制优先级功能虽然简单但足够解决很多实际问题。我之所以比较认可 Easy Rules是因为它把“规则从业务代码里拆出来”这件事做到了最低成本。你可以把规则定义存到数据库里启动时构建成 Easy Rules 的 Rule 对象修改规则不必发版。当然它不会帮你解决规则之间的复杂推理、不会自动管理规则的触发链规则多了以后性能需要靠压测确认。它更适合中小团队、规则数量可控、核心诉求是“少发版”的场景。3.3 Aviator 与 QLExpress表达式引擎不是规则引擎但对付简单规则很实用Aviator 和 QLExpress 本质上都是表达式引擎不是规则引擎但它们经常出现在规则引擎选型讨论里。原因很简单很多所谓规则本质就是一条带 if 逻辑的表达式。Aviator 性能高、语法轻量适合做条件判断和公式计算QLExpress 是阿里开源语法更接近 Java支持自定义函数、脚本块灵活度更高。如果你把规则拆成“条件表达式”和“动作表达式”存在数据库里再写一个轻量执行器完全可以自建一套很轻的规则服务。关键是规则之间大多是相互独立的没有复杂优先级和依赖关系。我在调研中发现很多团队最终落地时并没有选 Drools 或 Easy Rules而是用表达式引擎自建了规则层。原因很朴素规则少、逻辑浅表达式引擎完全够用还不需要引入一套复杂框架。但如果你期待引擎帮你解决规则冲突、循环触发、依赖分析表达式引擎做不到这部分只能自己写。3.4 LiteFlow从流程编排角度解决“规则怎么组织”LiteFlow 的定位和上面几个不太一样。它本质是流程编排框架核心思想是把业务拆成组件用 DSL 声明组件的执行顺序和跳转条件。在规则引擎调研里LiteFlow 往往作为“规则编排”的备选方案出现。比如风控链路先加载用户信息再判断是否命中黑名单如果不命中再判断额度最后决定放行或转人工。这种链路如果用 if-else 串起来逻辑不难但不好维护用 LiteFlow 可以把每一步变成独立组件用编排 DSL 控制顺序。它的优点是流程可视化、组件可以热更新、性能很高。但它不是推理式规则引擎规则之间没有复杂的冲突解决机制一切靠编排顺序表达。如果你的规则天然是流程式的LiteFlow 会让你感觉很顺手但如果你有成百上千条相互独立的 if 规则硬往编排上套只会更别扭。3.5 横向对比表与调研结论下面这张表浓缩了我对几类方案的判断方便快速回顾。维度DroolsEasy RulesAviator / QLExpressLiteFlow定位完整规则引擎轻量规则引擎表达式/动态脚本引擎流程编排框架规则组织方式DRL、决策表Java注解、代码表达式字符串组件加编排DSL推理能力Rete算法、议程组基础优先级无规则间关系管理无推理机制热更新能力支持KieScanner支持重新加载支持编译缓存支持组件热更新学习成本高低很低中等适合场景大型复杂规则集中小项目规则隔离简单条件计算流程型规则链路调研结论也比较明确规则数量少且相互独立优先考虑表达式引擎规则数量多、关系复杂、需要统一管理再考虑 DroolsEasy Rules 是两者之间的平衡点如果业务本身是流程式规则LiteFlow 是个不错的视角。4. 选型判断与落地路径我们选了哪条路以及怎么切4.1 为什么我们没有直接上 Drools一开始我也动过直接上 Drools 的念头毕竟它是公认的完整规则引擎资料多、案例也多。但结合我们团队的实际情况反复评估后我把这个方案否掉了。我们当前的业务规则数量只有几十条绝大多数是“新客判断”“金额阈值”“渠道白名单”这类单条件规则规则之间基本是并列关系没有复杂的触发链和冲突需要引擎推理。Drools 的 Rete 算法、议程组、激活组等能力对我们现有问题属于高配团队还要额外熟悉 DRL 语法和 Kie 容器生命周期运维成本明显增加。最终我们选择的是“Aviator 表达式引擎 自建规则表 轻量执行器”的组合。Aviator 负责条件表达式的解析和执行规则表负责存储规则元数据、优先级和版本执行器负责从规则表加载规则、按场景执行并记录日志。这个方案在保证灵活性的同时把技术复杂度压到了最低。4.2 试点范围怎么切才不至于翻车选型完成后最难的不是写代码而是决定从哪里切。我强烈建议不要一开始就把核心链路迁移到规则引擎。我们选择的试点规则是“新客专享券”发放它低频、影响面小、逻辑简单最适合验证整条链路。具体流程是先在原有代码旁新增规则引擎分支通过开关控制走新逻辑还是旧逻辑if (ruleEngineSwitch.isOn(order_new_user_discount)) { result ruleService.execute(order_discount, order); } else { result oldService.calculate(order); }线上先让规则引擎和新逻辑并行跑一段时间每次执行都记录新旧结果。有差异时第一时间打印规则命中日志和事实快照。跑通后再按用户维度灰度放量先放内部账号再放小流量最后全量切换。这样即使规则引擎有边界问题也能在放大流量前被及时发现。4.3 规则的存储与发布流程规则不能散落在配置文件里那样跟写在代码里区别不大。我们在数据库中建了一张规则表核心字段包括场景 code、规则名称、优先级、条件表达式、动作参数、版本号、生效开始时间、生效结束时间、状态。发布流程上每一次规则修改都生成新版本不覆盖旧记录。上线前通过后台做表达式语法校验和试算确认结果符合预期后再发布。发布时引擎只需要根据场景 code 和当前时间选择状态为启用且时间窗内最新版本即可。回滚更简单把指向的版本号改回旧版本规则内容不需要重新发布。这套设计本质上就是把“规则”当成数据资产来管理而不是当成代码来管理。开发人员只需要关心规则执行器的稳定性业务人员通过后台配置规则内容双方各司其职。5. 落地阶段真正让人头疼的细节5.1 规则版本管理与灰度发布规则引擎落地过程中最先暴露的问题往往不是性能而是规则版本混乱。我见过有的团队直接在数据库里修改规则字段改完也看不出来到底改了什么。上线后出了问题只能靠回忆找回之前的配置。所以规则版本化必须从第一天就做好。我的做法是规则表里每次变更都新增一条记录保留完整的历史版本。版本号由数据库自增或通过版本序列生成不允许更新已发布记录。规则引擎加载时会根据场景 code、当前时间和状态找到唯一生效的版本。同时我会把规则内容做一次 hash用来在缓存失效时快速校验版本一致性避免缓存里面是旧规则、数据库里面是新规则的情况。灰度发布方面规则引擎本身和业务应用是绑在一起的所以规则灰度要借助业务侧的用户维度和渠道维度。比如只对部分 user_id 尾号放量只对指定渠道生效。不要想着让规则引擎自己去灰度它只负责执行灰度策略应该放在接入层。5.2 性能、缓存与动态加载表达式引擎虽然轻量但也不能每次都重新编译表达式。Aviator 支持把编译后的表达式缓存起来我们会在缓存 key 里带上场景 code、版本号和表达式 hash这样规则更新后缓存 key 自然失效不会出现新规则不生效的诡异问题。规则执行器本身要做到无状态。每一条规则执行时只接收事实对象执行完返回结果不持有业务状态。动作部分尽量只做计算和结果标记不要把下单、发券这类副作用写在规则内部。真正的外部副作用放到执行器外面等规则执行完再统一处理。这样做的好处是规则引擎可以随时重跑、试算和回放而不会重复发券。压测也很有必要。虽然 Aviator 性能很高但规则动作里如果有远程调用整体延迟就会不可控。我们要求规则表达式只能访问传入的 Fact 对象不允许在表达式内部发起 HTTP 或者 RPC。这样规则执行耗时基本可控压测数字也稳定。5.3 调试与全链路日志规则引擎最怕的是什么一条规则线上没命中但没人知道为什么。所以在落地阶段我最早做的是规则命中日志。日志里至少要包含场景 code、规则名称、规则版本、关键事实摘要、执行耗时和 traceId。这样用户反馈订单没享受到优惠时我们能快速查出来到底是规则没有加载还是没有匹配还是动作执行失败。除了日志我还加了一个“试算接口”。运营在后台可以输入模拟的订单和用户信息引擎用当前最新版规则执行一遍返回命中了哪些规则、执行了哪些动作、最终结果是什么。这个接口不产生任何副作用纯粹做计算。它让业务人员改完规则后能自己验证不用每次找开发陪跑。试算接口看起来很基础但实际使用后能省掉很多沟通成本。5.4 权限与审计规则直接关联优惠、扣减、风控所以不能让人人都能改。我在配置后台做了三档权限查看、编辑、发布。查看权限给所有相关角色编辑权限给产品运营发布权限只给规则管理员和开发负责人。每次规则修改都必须记录操作者、操作时间、变更前后的完整规则内容。规则发布前还要经过一次审批审批人不是走形式而是要对规则的业务结果负责。规则引擎本身不判断业务合理性问题比如满减金额能不能是负数、折扣比例是否超限这些校验要在配置写入时拦住。这一步看起来繁琐但实际发生资损事故后你会庆幸每一条规则变更都有据可查。没有审计的规则引擎就像没有日志的线上系统出事之后只能靠猜。最后说一个我自己的体会。规则引擎不会让混乱的业务规则自动变得清晰它只是把规则从代码里搬到了配置层。如果规则本身已经失控换任何引擎都只是换一种方式乱。判断要不要引入先看两个指标规则多久变一次、改规则的人是谁。一个月都改不了一次代码加枚举就够一周改好几次且希望运营自己动手规则引擎才真的值得投入。这套调研和落地方法希望对同样在纠结的你有一点参考。