1. 背景

场景1:魔卡中有一个闯关成功获得经验值的场景,并且根据经验值范围确定用户的段位:

段位 经验值
青铜1 0~50
青铜2 50~300
青铜3 300~550
白银1 550~800
白银2 800~1050
白银3 1050~1300
黄金1 1300~1550
黄金2 1550~1800
黄金3 1800~2050
钻石1 2050~2300
钻石2 2300~2550
钻石3 2550~2800
钻石4 2800~3050
钻石5 3050~3550
王者1 3550~4050
王者2 4050~4550
王者3 4550~5050
……
王者N 每获得500经验值可升一级
N最大值为99

场景2:根据作答正确率确定星级:作答正确率达到20%,1颗星;作答正确率达到50%,2颗星;作答正确率达到80%,3颗星。

正确率 星级
0-20 0
20-50 1
50-80 2
80-100 3

2. 高效解决规则和结果的关系

我们当然可以通过写很多的 if else 判断用户的经验值范围或正确率范围,来确定段位和星级。主要有2个弊端:

  1. if else太多,程序可读性不高。
  2. 规则和代码耦合,规则可能随时变化,写死在程序里,不好。

解决方案:条件配置化 + 规则引擎

3. 规则引擎选型

常见的规则引擎有:Drools,JRules,Easy Rules,Aviator,仅从框架的易用性和轻量级角度考虑,最终考虑使用 Aviator 规则引擎(高性能、轻量级)。

4. Aviator规则引擎的应用

举例说明:根据作答正确率确定星级

正确率 星级
0-20 0
20-50 1
50-80 2
80-100 3

4.1 规则配置

将业务规则配置到配置中心,规则变更,实时感知。

  1. [
  2. {
  3. "exp": "accuracy < 20",
  4. "res": 0
  5. },
  6. {
  7. "exp": "accuracy >= 20 && accuracy < 50",
  8. "res": 1
  9. },
  10. {
  11. "exp": "accuracy >= 50 && accuracy < 80",
  12. "res": 2
  13. },
  14. {
  15. "exp": "accuracy >= 80",
  16. "res": 3
  17. }
  18. ]

4.2 规则本地化存储和解析

  1. package com.hwl.moka.service.service.challenge;
  2. import com.beust.jcommander.internal.Lists;
  3. import com.ctrip.framework.apollo.model.ConfigChangeEvent;
  4. import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
  5. import com.hwl.moka.bean.enums.BizNotifyTypeEnum;
  6. import com.hwl.moka.bean.vo.challenge.*;
  7. import com.hwl.moka.common.config.KVConfig;
  8. import com.hwl.moka.common.strategy.biznotify.BizNotifyContext;
  9. import com.hwl.moka.common.strategy.biznotify.BizNotifyHolder;
  10. import com.hwl.moka.common.utils.JacksonUtil;
  11. import lombok.extern.slf4j.Slf4j;
  12. import org.springframework.beans.factory.InitializingBean;
  13. import org.springframework.beans.factory.annotation.Autowired;
  14. import org.springframework.stereotype.Service;
  15. import java.util.List;
  16. import java.util.concurrent.locks.ReadWriteLock;
  17. import java.util.concurrent.locks.ReentrantReadWriteLock;
  18. /**
  19. * @description:闯关挑战配置本地化缓存服务
  20. * @author: zhangyu
  21. * @Date: 2021/2/26 3:46 下午
  22. */
  23. @Slf4j
  24. @Service
  25. public class ChallengeConfigLocalCacheService implements InitializingBean {
  26. @Autowired
  27. private KVConfig kvConfig;
  28. @Autowired
  29. private BizNotifyHolder bizNotifyHolder;
  30. private static final String calChallengeStarRuleKey = "cal_challenge_star_rule";
  31. // 可重入读写锁
  32. private ReadWriteLock lock = new ReentrantReadWriteLock();
  33. /**
  34. * 闯关获得星级规则配置
  35. */
  36. public List<CalChallengeStarRuleVO> calChallengeStarRuleList = Lists.newArrayList();
  37. /**
  38. * @description:读取配置并写入配置对象
  39. * @author:dongchenxu
  40. * @date:2021/3/10 2:22 下午
  41. */
  42. @Override
  43. public void afterPropertiesSet() throws Exception {
  44. loadCalChallengeStarRule(kvConfig.calChallengeStarRule);
  45. }
  46. /**
  47. * @description:监听配置变更,刷新配置对象
  48. * @author:dongchenxu
  49. * @date:2021/3/10 2:21 下午
  50. */
  51. @ApolloConfigChangeListener(value = {"application", "props"})
  52. public void watchConfigChange(ConfigChangeEvent changeEvent) {
  53. if (changeEvent.isChanged(calChallengeStarRuleKey)) {
  54. String newValue = changeEvent.getChange(calChallengeStarRuleKey).getNewValue();
  55. log.info("cal_challenge_star_rule changed. new value = {}", newValue);
  56. loadCalChallengeStarRule(newValue);
  57. }
  58. }
  59. /**
  60. * @description:从配置中心读取配置并本地化存储
  61. * @author:dongchenxu
  62. * @date:2021/3/10 2:21 下午
  63. */
  64. private void loadCalChallengeStarRule(String value) {
  65. try {
  66. lock.writeLock().lock();
  67. this.calChallengeStarRuleList = JacksonUtil.json2List(value, CalChallengeStarRuleVO.class);
  68. } catch (Exception exception) {
  69. BizNotifyContext context = new BizNotifyContext();
  70. context.setNotifyType(BizNotifyTypeEnum.ZYL.getCode());
  71. context.setTitle("加载配置cal_challenge_star_rule异常");
  72. context.setDetail("cal_challenge_star_rule:" + value);
  73. bizNotifyHolder.handle(context);
  74. } finally {
  75. lock.writeLock().unlock();
  76. }
  77. }
  78. /**
  79. * @description:读取配置
  80. * @author:dongchenxu
  81. * @date:2021/3/10 2:21 下午
  82. */
  83. public List<CalChallengeStarRuleVO> readCalChallengeStarRule() {
  84. try {
  85. lock.readLock().lock();
  86. return this.calChallengeStarRuleList;
  87. } finally {
  88. lock.readLock().unlock();
  89. }
  90. }
  91. }

4.3 规则匹配

  1. public Integer getChallengeStar (Integer accuracy) {
  2. List<CalChallengeStarRuleVO> challengeStarRuleList = challengeConfigLocalCacheService.readCalChallengeStarRule();
  3. for (CalChallengeStarRuleVO calChallengeStarRule : challengeStarRuleList) {
  4. String exp = calChallengeStarRule.getExp();
  5. Map<String, Object> map = new HashMap(1);
  6. map.put("accuracy", accuracy);
  7. Object res = AviatorEvaluator.execute(exp, map);
  8. Boolean valid = (Boolean) res;
  9. if (valid) {
  10. return calChallengeStarRule.getRes();
  11. }
  12. }
  13. return 0;
  14. }

5. Aviator特性和引擎模式

5.1 Aviator特性

Aviator是一个高性能、轻量级的 java 语言实现的表达式求值引擎.
运行方式:其他轻量级的求值器一般都是解释执行, 而Aviator则是直接将表达式编译成 JVM 字节码, 交给 JVM 去执行。
特性:

  1. 支持绝大多数运算操作符,包括算术操作符、关系运算符、逻辑操作符、位运算符、正则匹配操作符(=~)、三元表达式
  2. .支持操作符优先级和括号强制设定优先级;
  3. 逻辑运算符支持短路运算;
  4. 支持丰富类型,例如nil、整数和浮点数、字符串、正则表达式、日期、变量等,支持自动类型转换;
  5. 内置一套强大的常用函数库;
  6. 可自定义函数,易于扩展;
  7. 可重载操作符;
  8. 支持大数运算(BigInteger)和高精度运算(BigDecimal);
  9. 性能优秀。

    5.2 引擎模式

    1、AviatorEvaluator.EVAL,默认值,以运行时的性能优先,编译会花费更多时间做优化,目前会做一些常量折叠、公共变量提取的优化。
    2、AviatorEvaluator.COMPILE,以编译的性能优先,不会做任何编译优化,牺牲一定的运行性能。
    修改默认引擎模式:AviatorEvaluator.getInstance().setOption(Options.OPTIMIZE_LEVEL, AviatorEvaluator.COMPILE);
    适用场景:
  • AviatorEvaluator.EVAL:适合长期运行的表达式。(表达式稳定不变)
  • AviatorEvaluator.COMPILE:适合需要频繁编译表达式的场景。(表达式变动频繁)

aviator 常用的两个方法:compile、execute

  1. /**
  2. * 编译
  3. */
  4. public static Expression compile(final String expression) {
  5. return compile(expression, false);
  6. }
  7. /**
  8. * 执行
  9. */
  10. public static Object execute(final String expression, final Map<String, Object> env) {
  11. return execute(expression, env, false);
  12. }

这种模式下有两个问题:

  1. 每次都重新编译,如果你的脚本没有变化,这个开销是浪费的,非常影响性能。
  2. 编译每次都产生新的匿名类,这些类会占用 JVM 方法区(Perm 或者 metaspace),内存逐步占满,并最终触发 full gc。

改为编译缓存模式:

  1. /**
  2. * 编译
  3. */
  4. public static Expression compile(final String expression, final boolean cached) {
  5. return getInstance().compile(expression, cached);
  6. }
  7. /**
  8. * 执行
  9. */
  10. public static Object execute(final String expression, final Map<String, Object> env, final boolean cached) {
  11. return getInstance().execute(expression, env, cached);
  12. }
  13. // 也可以在启动类中一次性设置:
  14. public class MokaServerApplication extends SpringBootServletInitializer {
  15. public static void main(String[] args) {
  16. AviatorEvaluator.getInstance().setCachedExpressionByDefault(true);
  17. SpringApplication.run(MokaServerApplication.class, args);
  18. }
  19. }

更多关于aviator的介绍:https://www.yuque.com/boyan-avfmj/aviatorscript

6. 未来展望

将规则引擎独立成一个服务,支撑集团中台所有的规则校验和计算服务。

7. 推荐阿里的 compileflow 流程引擎

https://www.oschina.net/p/compileflow
compileflow是一个非常轻量、高性能、可集成、可扩展的流程引擎。
compileflow Process引擎是淘宝工作流TBBPM引擎之一,是专注于纯内存执行,无状态的流程引擎,通过将流程文件转换生成java代码编译执行,简洁高效。
compileflow能让开发人员通过流程编辑器设计自己的业务流程,将复杂的业务逻辑可视化,为业务设计人员与开发工程师架起了一座桥梁。