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
@Service
public class ChallengeConfigLocalCacheService implements InitializingBean {
@Autowired
private KVConfig kvConfig;
@Autowired
private 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 下午
*/
@Override
public 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
能让开发人员通过流程编辑器设计自己的业务流程,将复杂的业务逻辑可视化,为业务设计人员与开发工程师架起了一座桥梁。