引入规则引擎方案

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

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);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容