在上一篇文章中,我介绍了在电商核心链路中引入 RoaringBitmap,
解决活动命中计算的性能与内存问题。
但当这部分性能瓶颈被解决之后,一个新的问题很快浮现出来:
计算已经足够快了,但规则正在变得越来越难以维护。
这篇文章想聊的,不是 RoaringBitmap 本身,
而是在它之后,规则是如何被一步步抽象、拆解、编排,
最终演进成一套可长期扩展的结构的。
一、规则是如何被“自然引入”系统的?
在业务初期,规则并不会一次性出现。
最早可能只有:
-
商品是否参与活动
-
活动是否限制区域
于是代码里很自然地出现了:
if (hitProduct && hitRegion) {
// 命中活动
}
后来,规则开始增加:
-
渠道限制
-
客户类型
-
黑白名单
-
特殊活动兜底逻辑
代码也随之演化成:
if (hitProduct
&& hitRegion
&& hitChannel
&& hitCustomer
&& !inBlackList) {
// 命中活动
}
规则不是设计出来的,而是被业务一点点“推”进系统的。
二、当规则变多时,最先失控的不是性能,而是结构
在引入 RoaringBitmap 之前,我们以为问题在性能;
但性能问题解决后,很快发现:
-
判断逻辑横向扩散
-
不同规则之间开始隐式耦合
-
新需求必须改旧代码
-
排障时很难定位是哪一条规则出了问题
这时的痛点已经变成:
规则本身已经失去了“边界”。
三、第一次关键抽象:规则 ≠ 判断条件
一次重要的认知转变是:
规则不是 if / else,
而是在特定上下文下,产出一个“命中结果”。
在活动场景中,这个结果是:
-
一批命中的活动 ID
而 RoaringBitmap,正好是这个结果的理想承载结构。
于是我们开始把规则抽象成一个统一概念:
👉 规则算子(Rule Operator)
每个规则算子只做一件事:
- 输入:统一的业务上下文
- 输出:一个命中集合(RBM)
- 不关心其他规则的存在
四、算子化设计:每个规则只负责“算自己”
最早的接口定义非常简单:
public interface RuleOperator {
RoaringBitmap match(RuleContext context);
}
对应的实现:
-
RegionRuleOperator
-
ProductRuleOperator
-
ChannelRuleOperator
-
CustomerTypeRuleOperator
它们之间没有任何调用关系,也不感知执行顺序。
规则第一次有了清晰的边界。
五、为什么引入抽象类,而不是只用接口?
随着规则继续增加,一个新问题出现了:
-
不同算子实现风格不一致
-
有的算子提前返回
-
有的算子夹杂日志、监控、兜底逻辑
接口只能约束“方法签名”,
却无法约束执行流程。
而在规则系统中,真正重要的是:
哪些步骤是必须的,哪些地方才允许扩展。
于是我们引入了 抽象类 + 模板方法。
六、用模板方法收住规则执行边界
抽象类的核心目标只有一个:
流程固定,变化收敛。
public abstract class AbstractRuleOperator {
public final RoaringBitmap match(RuleContext ctx) {
if (!support(ctx)) {
return RoaringBitmap.bitmapOf();
}
return doMatch(ctx);
}
protected abstract boolean support(RuleContext ctx);
protected abstract RoaringBitmap doMatch(RuleContext ctx);
}
这里有几个关键点:
-
match 被 final 修饰,流程不可被破坏
-
子类只能实现自己的命中逻辑
-
是否参与计算由 support 决定
抽象类不是为了复用代码,而是为了限制自由。
七、规则编排:把“顺序”和“组合”从规则中拿走
当规则都被算子化之后,下一步是一个非常重要的决定:
规则本身不应该知道执行顺序,也不应该关心如何组合。
于是我们引入了独立的 规则编排层。
public class RulePipeline {
private final List<AbstractRuleOperator> operators;
public RoaringBitmap execute(RuleContext ctx) {
RoaringBitmap result = null;
for (AbstractRuleOperator operator : operators) {
RoaringBitmap hit = operator.match(ctx);
result = (result == null) ? hit : RoaringBitmap.and(result, hit);
}
return result;
}
}
这样做的好处非常明显:
- 顺序由编排层控制
- 算子只关心“我能命中什么”
- 新规则只需插入 pipeline
八、RoaringBitmap 在这里扮演的真实角色
在这套结构中,RoaringBitmap 的定位非常清晰:
-
不是规则引擎
-
不是业务抽象
-
而是 高效、稳定的集合计算底座
它解决的是:
当规则被拆得足够细时,
集合计算不会成为系统瓶颈。
九、自然扩展:优惠券规则并没有“另起一套系统”
当活动规则稳定之后,我们发现:
优惠券场景在本质上是同一类问题。
区别只是:
-
规则维度不同
-
输出集合不同
-
编排策略略有差异
于是我们:
-
复用同一套算子模型
-
复用同一套编排机制
-
只扩展新的算子实现
不是我们为优惠券设计了系统,
而是规则系统自然“长”到了优惠券场景。
十、这套设计带来的真实收益
落地一段时间后,这套结构带来的变化非常直观:
- 新规则 = 新算子
- 旧规则几乎不需要改动
- 单个规则可独立测试
- 排障成本显著降低
十一、最后的总结
可以用一句话概括这次演进:
RBM 解决的是“算得快”,
规则算子解决的是“各算各的”,
编排层解决的是“怎么一起算”。
当这三层边界清晰之后,
系统的复杂度,才真正被关进了笼子里。
结语
很多系统之所以变复杂,
并不是因为技术不够先进,
而是因为:
规则在不断增长,却从未被认真抽象过。
这次实践中,真正起决定性作用的,
不是某个具体工具,
而是对边界的持续克制。