活动底盘引入规则引擎方案
1.背景
目前上游很多场景是根据客群A发礼包1,客群B发礼包2,这种情况下,这些判断条件大部分是:客群、会员等级、风控等级。但是由于规则只支持固定的活动编码 + 规则编码对应到固定的奖池,没法实现根据这些因子去发不同的奖池对应的礼包。
问题:这样就会导致每次上游都需要重复实现根据会员等级、客群等条件来判断发哪个礼包
基于这个场景,我们引入了规则因子多奖池的方案来解决上面的问题。
2.整体改动梳理
目前:
优化后:
改动点:
1.引入多奖池概念:一起一个规则对应一个奖池,现在一个规则对应多个奖池
2.引入规则引擎:通过将奖池的条件因子组合,生成规则引擎表达式,运行,最终通过运行确定哪个奖池被命中
3.规则引擎对比
规则引擎 | 功能完整度 | 性能 | 上手简易程度 |
---|---|---|---|
LiteFlow | 中等 | 一般 | 简单 |
Drools | 高 | 高 | 复杂 |
EasyRules | 低 | 高 | 简单 |
Aviator | 中等 | 高 | 简单 |
功能完整度:LiteFlow适用于基本的规则匹配和流程控制,相对功能较为简单;Drools具有强大的规则编写和管理功能,支持复杂的规则逻辑;EasyRules提供了最基础的规则引擎功能,适用于简单场景;Aviator提供了更丰富的函数库和表达式语言支持,适用于更复杂的业务需求。
性能:Drools在性能方面表现出色,尤其在处理大量数据时效率高;EasyRules和Aviator也有不错的性能表现;LiteFlow相对较低,处理速度可能会受到影响。
上手简易程度:LiteFlow具有良好的可读性和可理解性,学习曲线相对较低;Drools在配置和规则编写上相对复杂,需要一定的学习成本;EasyRules和Aviator都非常简单易用,入门门槛较低。
根据上面的对比完后,我们很重要的一点就是需要支持手动传入表达式,并且足够简单高效。最终Aviator
是最最符合要求的,选择了Aviator
规则引擎。
4.实现方案
整体方案流程图:
1.如何生成规则引擎表达式?
可执行的表达式:
(userGroup("1-1") && memberLv("1-2")) || (userGroup("2-1") && memberLv("2-2"))
生成有3种方案:
xml配置:写xml文件集成在项目里面,但是这种方式不适合我们项目这种由页面生成的方式。
前端生成:构建出下面的条件,传给后端保存起来,直接运行即可。
后端生成:通过定义好DTO,每组和每个集合之间的关系,后端代码构建出表达式。
DTO对象:
[
{
"ruleFactors": [
{
"handlerType": "USER_GROUP",
"relationStatus": 1,
"userGroup": {
"groupType": 1,
"userGroupId": 0,
"userGroupName": "客群A",
"bdpCode": "xxxxxx"
}
},
{
"handlerType": "MEMBER_LEVEL",
"relationStatus": 1,
"memberLvs": [
"2",
"3"
]
}
],
"innerGroupOptType": "or"
},
{
"ruleFactors": [
{
"handlerType": "RISK_LEVEL",
"relationStatus": 1,
"riskLevels": [
"reject",
"review"
]
}
],
"innerGroupOptType": "or",
"preGroupOptType": "and"
}
]
最终选择后端生成,同时兼顾了以后前端复杂的场景后,我们直接使用后端生成的规则引擎表达式。
2.一次运行涉及的代码
1.注册函数
// 引入函数
AviatorEvaluator.addFunction(new MemberLevelFactorHandler(applicationContext));
AviatorEvaluator.addFunction(new RiskLevelFactorHandler(applicationContext));
AviatorEvaluator.addFunction(new UserGroupFactorHandler(applicationContext));
2.编译表达式
AviatorEvaluator.compile(expressStr.toString());
3.执行
try {
ruleResult = (Boolean) expression.execute(env);
} catch (ExpressionRuntimeException e) {
Throwable cause = e.getCause();
if (cause instanceof BusinessException) {
throw (BusinessException) cause;
} else {
log.error("", e);
BusinessAssert.error(ErrorResponseEnum.EXEC_RULE_ENGINE_EXPRESS_ERROR);
}
}
3.参数传递
-
由于规则引起执行的是表达式,我们将表达式传入规则引擎后,编译,运行,这种情况下,我们的规则因子的java dto参数将无法传递。
解决:采用将参数当做规则引擎的上下文传入,并且生成表达式的时候,生成参数的索引,等使用的时候,再根据下标挨个读取出来。
传入:
Map<String, Object> env = new HashMap<>();//注意一定要是<String,Object>的
// 放上下文和规则需要的固定参数
env.put("hitFactorContext", hitFactorContext);
env.put("ruleFactorGroups", ruleFactorGroups);
Boolean ruleResult = null;
try {
ruleResult = (Boolean) expression.execute(env);
} catch (ExpressionRuntimeException e) {
// xxx
}
取出:
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
HitRuleFactorContextDTO factorContext = (HitRuleFactorContextDTO) env.get("hitFactorContext");
List<RuleFactorGroupDTO> ruleFactorGroups = (List<RuleFactorGroupDTO>) env.get("ruleFactorGroups");
// 取出对应的参数
String groupFactorIdxStr = (String) arg1.getValue(null);
List<String> idxList = SfStrUtil.split(groupFactorIdxStr, '-');
Integer groupIdx = Integer.valueOf(idxList.get(0));
Integer factorIdx = Integer.valueOf(idxList.get(1));
RuleFactorDTO ruleFactor = ruleFactorGroups.get(groupIdx).getRuleFactors().get(factorIdx);
Boolean include = include(ruleFactor, factorContext);
boolean result = Objects.equals(ruleFactor.getRelationStatus(), 1) ? include : !include;
return AviatorBoolean.valueOf(result);
}
- spring的上下文:
在创建自定义函数对象的时候,同时也先加入。
6.性能优化
1. 执行/查询 性能
针对每个用户查询命中的奖池,根据
ruleCode + userId
做了5分钟的redis缓存编译好的表达式缓存
2. 内存方面
采用本地缓存把编译好的规则引擎表达式缓存起来,防止每次都构建。
虽然aviator
规则引擎编译好的表达式可以支持本地缓存,但是它是直接用的Map缓存,没有过期时间,有一定的内存风险,所以这里使用Caffeine
expressCache = Caffeine.newBuilder()
//初始数量
.initialCapacity(64)
//最大条数
.maximumSize(2048)
//PS:expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
//读后20s过期
.expireAfterAccess(20, TimeUnit.SECONDS)
.build();
3. 重复调用优化
在一次规则匹配中,可能会存在对同一客群多次判断是否存在、以及多次查询同一用户的风控等级等情况,我们同样做了优化,在本次匹配中,会将查询、匹配结果存到上下文,后续再访问到即可直接获取结果,无须再次调用。
- 会员信息:
// 判断不存在
if (SfStrUtil.isBlank(factorContext.getGradeVal())) {
queryAndFillUserInfo(factorContext);
}
protected void queryAndFillUserInfo(HitRuleFactorContextDTO factorContext) {
// 求值,并且把值塞进去,方便后续使用
UserInfoDTO userInfo = applicationContext.getBean(UserManager.class).getUserInfo(factorContext.getUserId());
BusinessAssert.isNotNull(userInfo, ErrorResponseEnum.USER_INFO_NOT_EXIST, userInfo.getUserId());
// 填充会员等级
if (SfStrUtil.isBlank(factorContext.getGradeVal())) {
factorContext.setGradeVal(userInfo.getGradeVal());
}
// 填充手机号
if (SfStrUtil.isBlank(factorContext.getMobile())) {
String encryptMobile = applicationContext.getBean(EncryptDecryptHelper.class).encryptMobile(userInfo.getMobile());
factorContext.setMobile(encryptMobile);
}
}
- 客群命中信息:
UserGroupFactorCacheDTO cacheDTO = new UserGroupFactorCacheDTO();
cacheDTO.setRuleCode(factorContext.getRuleCode());
cacheDTO.setMobile(factorContext.getMobile());
cacheDTO.setGroupType(userGroupFactor.getGroupType());
cacheDTO.setUserGroupId(userGroupFactor.getUserGroupId());
cacheDTO.setUsergroupName(userGroupFactor.getUserGroupName());
cacheDTO.setBdpCode(userGroupFactor.getBdpCode());
// 判断是之前命中过,命中过的话,直接返回
Boolean cacheIncludeResult = userGroupHitMap.get(cacheDTO);
if (cacheIncludeResult != null) {
return cacheIncludeResult;
}
UserGroupInfoDTO userGroup = new UserGroupInfoDTO();
userGroup.setGroupType(userGroupFactor.getGroupType());
userGroup.setUserGroupId(userGroupFactor.getUserGroupId());
userGroup.setUsergroupName(userGroupFactor.getUserGroupName());
userGroup.setBdpCode(userGroupFactor.getBdpCode());
boolean include = applicationContext.getBean(UserGroupManager.class)
.include(userGroup, factorContext.getMobile(), factorContext.getRuleCode());
// 缓存,方便后续有需要的判断使用
userGroupHitMap.put(cacheDTO, include);