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个弊端:
- if else太多,程序可读性不高。
- 规则和代码耦合,规则可能随时变化,写死在程序里,不好。
解决方案:条件配置化 + 规则引擎
3. 规则引擎选型
常见的规则引擎有:Drools,JRules,Easy Rules,Aviator,仅从框架的易用性和轻量级角度考虑,最终考虑使用 Aviator 规则引擎(高性能、轻量级)。
4. Aviator规则引擎的应用
举例说明:根据作答正确率确定星级
| 正确率 | 星级 |
|---|---|
| 0-20 | 0 |
| 20-50 | 1 |
| 50-80 | 2 |
| 80-100 | 3 |
4.1 规则配置
将业务规则配置到配置中心,规则变更,实时感知。
[{"exp": "accuracy < 20","res": 0},{"exp": "accuracy >= 20 && accuracy < 50","res": 1},{"exp": "accuracy >= 50 && accuracy < 80","res": 2},{"exp": "accuracy >= 80","res": 3}]
4.2 规则本地化存储和解析
package com.hwl.moka.service.service.challenge;import com.beust.jcommander.internal.Lists;import com.ctrip.framework.apollo.model.ConfigChangeEvent;import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;import com.hwl.moka.bean.enums.BizNotifyTypeEnum;import com.hwl.moka.bean.vo.challenge.*;import com.hwl.moka.common.config.KVConfig;import com.hwl.moka.common.strategy.biznotify.BizNotifyContext;import com.hwl.moka.common.strategy.biznotify.BizNotifyHolder;import com.hwl.moka.common.utils.JacksonUtil;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.InitializingBean;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/*** @description:闯关挑战配置本地化缓存服务* @author: zhangyu* @Date: 2021/2/26 3:46 下午*/@Slf4j@Servicepublic class ChallengeConfigLocalCacheService implements InitializingBean {@Autowiredprivate KVConfig kvConfig;@Autowiredprivate BizNotifyHolder bizNotifyHolder;private static final String calChallengeStarRuleKey = "cal_challenge_star_rule";// 可重入读写锁private ReadWriteLock lock = new ReentrantReadWriteLock();/*** 闯关获得星级规则配置*/public List<CalChallengeStarRuleVO> calChallengeStarRuleList = Lists.newArrayList();/*** @description:读取配置并写入配置对象* @author:dongchenxu* @date:2021/3/10 2:22 下午*/@Overridepublic void afterPropertiesSet() throws Exception {loadCalChallengeStarRule(kvConfig.calChallengeStarRule);}/*** @description:监听配置变更,刷新配置对象* @author:dongchenxu* @date:2021/3/10 2:21 下午*/@ApolloConfigChangeListener(value = {"application", "props"})public void watchConfigChange(ConfigChangeEvent changeEvent) {if (changeEvent.isChanged(calChallengeStarRuleKey)) {String newValue = changeEvent.getChange(calChallengeStarRuleKey).getNewValue();log.info("cal_challenge_star_rule changed. new value = {}", newValue);loadCalChallengeStarRule(newValue);}}/*** @description:从配置中心读取配置并本地化存储* @author:dongchenxu* @date:2021/3/10 2:21 下午*/private void loadCalChallengeStarRule(String value) {try {lock.writeLock().lock();this.calChallengeStarRuleList = JacksonUtil.json2List(value, CalChallengeStarRuleVO.class);} catch (Exception exception) {BizNotifyContext context = new BizNotifyContext();context.setNotifyType(BizNotifyTypeEnum.ZYL.getCode());context.setTitle("加载配置cal_challenge_star_rule异常");context.setDetail("cal_challenge_star_rule:" + value);bizNotifyHolder.handle(context);} finally {lock.writeLock().unlock();}}/*** @description:读取配置* @author:dongchenxu* @date:2021/3/10 2:21 下午*/public List<CalChallengeStarRuleVO> readCalChallengeStarRule() {try {lock.readLock().lock();return this.calChallengeStarRuleList;} finally {lock.readLock().unlock();}}}
4.3 规则匹配
public Integer getChallengeStar (Integer accuracy) {List<CalChallengeStarRuleVO> challengeStarRuleList = challengeConfigLocalCacheService.readCalChallengeStarRule();for (CalChallengeStarRuleVO calChallengeStarRule : challengeStarRuleList) {String exp = calChallengeStarRule.getExp();Map<String, Object> map = new HashMap(1);map.put("accuracy", accuracy);Object res = AviatorEvaluator.execute(exp, map);Boolean valid = (Boolean) res;if (valid) {return calChallengeStarRule.getRes();}}return 0;}
5. Aviator特性和引擎模式
5.1 Aviator特性
Aviator是一个高性能、轻量级的 java 语言实现的表达式求值引擎.
运行方式:其他轻量级的求值器一般都是解释执行, 而Aviator则是直接将表达式编译成 JVM 字节码, 交给 JVM 去执行。
特性:
- 支持绝大多数运算操作符,包括算术操作符、关系运算符、逻辑操作符、位运算符、正则匹配操作符(=~)、三元表达式
- .支持操作符优先级和括号强制设定优先级;
- 逻辑运算符支持短路运算;
- 支持丰富类型,例如nil、整数和浮点数、字符串、正则表达式、日期、变量等,支持自动类型转换;
- 内置一套强大的常用函数库;
- 可自定义函数,易于扩展;
- 可重载操作符;
- 支持大数运算(BigInteger)和高精度运算(BigDecimal);
- 性能优秀。
5.2 引擎模式
1、AviatorEvaluator.EVAL,默认值,以运行时的性能优先,编译会花费更多时间做优化,目前会做一些常量折叠、公共变量提取的优化。2、AviatorEvaluator.COMPILE,以编译的性能优先,不会做任何编译优化,牺牲一定的运行性能。
修改默认引擎模式:AviatorEvaluator.getInstance().setOption(Options.OPTIMIZE_LEVEL, AviatorEvaluator.COMPILE);
适用场景:
AviatorEvaluator.EVAL:适合长期运行的表达式。(表达式稳定不变)AviatorEvaluator.COMPILE:适合需要频繁编译表达式的场景。(表达式变动频繁)
aviator 常用的两个方法:compile、execute
/*** 编译*/public static Expression compile(final String expression) {return compile(expression, false);}/*** 执行*/public static Object execute(final String expression, final Map<String, Object> env) {return execute(expression, env, false);}
这种模式下有两个问题:
- 每次都重新编译,如果你的脚本没有变化,这个开销是浪费的,非常影响性能。
- 编译每次都产生新的匿名类,这些类会占用 JVM 方法区(Perm 或者 metaspace),内存逐步占满,并最终触发 full gc。
改为编译缓存模式:
/*** 编译*/public static Expression compile(final String expression, final boolean cached) {return getInstance().compile(expression, cached);}/*** 执行*/public static Object execute(final String expression, final Map<String, Object> env, final boolean cached) {return getInstance().execute(expression, env, cached);}// 也可以在启动类中一次性设置:public class MokaServerApplication extends SpringBootServletInitializer {public static void main(String[] args) {AviatorEvaluator.getInstance().setCachedExpressionByDefault(true);SpringApplication.run(MokaServerApplication.class, args);}}
更多关于aviator的介绍:https://www.yuque.com/boyan-avfmj/aviatorscript
6. 未来展望
将规则引擎独立成一个服务,支撑集团中台所有的规则校验和计算服务。
7. 推荐阿里的 compileflow 流程引擎
https://www.oschina.net/p/compileflowcompileflow是一个非常轻量、高性能、可集成、可扩展的流程引擎。compileflow Process引擎是淘宝工作流TBBPM引擎之一,是专注于纯内存执行,无状态的流程引擎,通过将流程文件转换生成java代码编译执行,简洁高效。compileflow能让开发人员通过流程编辑器设计自己的业务流程,将复杂的业务逻辑可视化,为业务设计人员与开发工程师架起了一座桥梁。
