人群组件与Drools规则引擎实践
这篇主要解决什么问题
我后来回头看这类活动系统,最容易失控的不是规则多,而是两层逻辑混在一起:
1、人群准入写在业务代码里
2、规则命中继续写在 if/else 里
3、阶段切换时又把条件复制一遍
4、最后没人说得清“这次为什么过了,为什么没过”
所以这类能力要先拆成两层:
1、人群组件负责准入、过滤、短路
2、Drools 负责组合条件匹配和决策输出
这不是概念拆分,而是为了让执行链路能解释、能扩展、能排查。
我理解的人群组件,应该只做过滤,不做决策
人群组件适合处理下面这类问题:
1、是否命中某个标签
2、是否在灰度、白名单、黑名单内
3、渠道、身份、地域是否允许进入
4、当前阶段是否允许继续往下执行
这层的特点是“过”或者“不过”,本质上是过滤器。
如果把下面这些也塞进人群组件,边界就会很快变糊:
1、新客且近 7 天未领券,命中 A 奖励
2、老客消费金额超过阈值,命中 B 奖励
3、命中标签后再结合库存、预算、时间窗决定动作
这些已经不是准入判断,而是规则决策。
人群组件的运行模型,核心是原子人群和逻辑人群
真正落地时,我更倾向把人群组件拆成两层:
1、原子人群:一个最小可执行判断,比如标签命中、渠道命中、身份命中
2、逻辑人群:把多个原子人群按 AND/OR/NOT 组合起来
运行时其实只需要区分两种执行策略:
1、ATOM_CROWD
2、LOGIC_CROWD
这两个枚举更像执行模型,不是业务阶段。
业务阶段应该放在规则组或阶段关系上,比如准入、玩法、发奖这些阶段,而不是直接写到人群定义里。
为什么阶段不要直接塞进人群定义
如果把阶段语义直接塞到人群本身,后面很容易遇到两个问题:
1、同一个人群定义无法复用到不同阶段
2、阶段一变,人群配置也要跟着改
更稳的方式通常是:
1、人群定义只描述“怎么判断”
2、阶段关系表描述“在哪个阶段执行”
3、规则组描述“执行完后进入哪组规则”
这样一来,人群定义、阶段装配、规则决策三层职责就分开了。
一条更像工程实现的执行链路
我更认可的链路是下面这样:
1、按阶段装配当前要执行的人群组件
2、先执行原子人群,再执行逻辑人群
3、只要当前阶段准入失败,直接短路返回
4、准入通过后,构建当前阶段的规则 facts
5、把 facts 放进 Drools session,执行对应规则组
6、把命中结果收口成统一的 RuleResult
这样做有两个直接收益:
1、准入失败和规则未命中可以明确区分
2、排查时能知道卡在人群层还是规则层
Drools 什么时候值得引入
Drools 不是有规则就一定要上。
我一般只在下面这些场景考虑它:
1、组合条件已经明显多维化
2、多条规则之间会持续新增、修改、下线
3、同一批事实可能命中多条规则
4、规则需要和主流程代码解耦
如果只是 2 到 3 个固定判断,普通代码通常更直接;但一旦条件组合开始上升,Drools 会比长串 if/else 更容易维护。
用 Drools 之前,先把 facts 设计好
Drools 能不能用得稳,关键不在 rule 文件,而在 facts 是否稳定。
我通常不会直接把数据库实体丢给 Drools,而是单独建一个规则事实对象:
1 | public class RuleFacts { |
这样做的目的很明确:
1、规则只依赖稳定字段,不直接依赖数据库结构
2、业务实体怎么演进,不会直接把 rule 搞崩
3、Drools 看到的是决策数据,不是持久化对象
一个最小可用的结果模型也要先定下来
只把 facts 扔进 session 还不够,还得先想清楚规则命中后要返回什么。
1 | public class RuleResult { |
没有统一结果结构时,最后往往会退化成:
1、规则里直接改各种上下文变量
2、主流程到处判断中间状态
3、线上看到“没命中”,但不知道是哪条规则在控制
一个更像实际使用的 Drools 规则示例
下面这个示例会比“判断新客送券”更接近实际一些:
1 | rule "play_stage_new_user_reward" |
这里有几个点是我觉得比较实用的:
1、agenda-group 把规则限制在当前阶段
2、salience 控制优先级
3、规则里只产出结果,不直接写库
4、命中理由也作为结果的一部分带出来
规则引擎负责决策,真正的写库、发奖、发消息这些动作,仍然应该回到主流程里处理。
Java 侧怎么执行 Drools
如果只是想先验证规则建模,我一般会先用最小方式把 session 跑起来:
1 | KieSession kieSession = kieContainer.newKieSession("ruleSession"); |
这个过程中最重要的不是 fireAllRules(),而是前后两个动作:
1、进 session 前把 facts 收干净
2、出 session 后把结果统一收口
如果 session 里既塞 facts 又塞数据库实体,还混着一堆可变上下文,后面几乎一定会越来越难维护。
我更偏好的收口方式
Drools 执行完后,我更愿意只拿统一结果,而不是让规则里直接改主流程对象。
1 | List<RuleResult> resultList = new ArrayList<>(); |
这样后面的主流程只关心两件事:
1、命中了什么
2、接下来执行什么动作
代码层和规则层的边界会清楚很多。
落地时最容易踩的几个坑
1、人群层和规则层同时做业务判断
人群组件里写一半决策,Drools 再补另一半,最后没有人能解释完整链路。
2、facts 直接绑定实体
实体字段一改,rule 文件就跟着脆。
3、规则里直接做副作用操作
如果在 rule 里直接写库、调下游、发消息,调试和回滚都会很麻烦。
4、规则拆得过碎
Drools 不是把每个小条件都拆一条 rule,拆太碎后,优先级和命中顺序会越来越难控。
5、没有解释结果
没有命中链路、规则编码、命中理由时,线上排查基本只能翻日志猜。
如果让我从零开始收这套能力
我的顺序通常会是:
1、先把人群组件收成原子 + 逻辑两层
2、再把阶段类型和规则组边界理清
3、再给 Drools 抽稳定的 facts 和 result 模型
4、最后才让复杂决策逐步迁进 rule 文件
这个顺序的好处是,不会一上来就把 Drools 推成一个大而全的平台,也不会把业务复杂度直接压进规则引擎。
小结
人群组件和 Drools 不是替代关系,而是前后两层。
人群组件负责把“不该进来的人”挡在前面,Drools 负责把“已经进来的人”按事实做组合决策。真正稳定的落地方式,不是写几条 rule,而是把阶段、facts、结果、执行边界这几件事先收清楚。



