引入规则引擎方案

活动底盘引入规则引擎方案

1.背景

目前上游很多场景是根据客群A发礼包1,客群B发礼包2,这种情况下,这些判断条件大部分是:客群、会员等级、风控等级。但是由于规则只支持固定的活动编码 + 规则编码对应到固定的奖池,没法实现根据这些因子去发不同的奖池对应的礼包。

问题:这样就会导致每次上游都需要重复实现根据会员等级、客群等条件来判断发哪个礼包

基于这个场景,我们引入了规则因子多奖池的方案来解决上面的问题。

2.整体改动梳理

目前:

image.png

优化后:

image.png

改动点:

1.引入多奖池概念:一起一个规则对应一个奖池,现在一个规则对应多个奖池

2.引入规则引擎:通过将奖池的条件因子组合,生成规则引擎表达式,运行,最终通过运行确定哪个奖池被命中

3.规则引擎对比

规则引擎 功能完整度 性能 上手简易程度
LiteFlow 中等 一般 简单
Drools 复杂
EasyRules 简单
Aviator 中等 简单
  • 功能完整度:LiteFlow适用于基本的规则匹配和流程控制,相对功能较为简单;Drools具有强大的规则编写和管理功能,支持复杂的规则逻辑;EasyRules提供了最基础的规则引擎功能,适用于简单场景;Aviator提供了更丰富的函数库和表达式语言支持,适用于更复杂的业务需求。

  • 性能:Drools在性能方面表现出色,尤其在处理大量数据时效率高;EasyRules和Aviator也有不错的性能表现;LiteFlow相对较低,处理速度可能会受到影响。

  • 上手简易程度:LiteFlow具有良好的可读性和可理解性,学习曲线相对较低;Drools在配置和规则编写上相对复杂,需要一定的学习成本;EasyRules和Aviator都非常简单易用,入门门槛较低。

根据上面的对比完后,我们很重要的一点就是需要支持手动传入表达式,并且足够简单高效。最终Aviator是最最符合要求的,选择了Aviator规则引擎。

4.实现方案

整体方案流程图:

image.png

1.如何生成规则引擎表达式?

可执行的表达式:

(userGroup("1-1") && memberLv("1-2")) || (userGroup("2-1") && memberLv("2-2"))

生成有3种方案:

  1. xml配置:写xml文件集成在项目里面,但是这种方式不适合我们项目这种由页面生成的方式。

  2. 前端生成:构建出下面的条件,传给后端保存起来,直接运行即可。

  3. 后端生成:通过定义好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.参数传递

  1. 由于规则引起执行的是表达式,我们将表达式传入规则引擎后,编译,运行,这种情况下,我们的规则因子的java dto参数将无法传递。


    image.png

解决:采用将参数当做规则引擎的上下文传入,并且生成表达式的时候,生成参数的索引,等使用的时候,再根据下标挨个读取出来。

image.png

传入:

  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);
      }
  1. 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);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容