【Aviator】(二)应用实战

        正如上一篇博客所讲:“项目中之前的计算公式统一维护在base包(base包的公式会被订单、产品、api、财务、还款等系统依赖),由于公式会频繁地改动,造成base频繁升级,其他系统也得跟着发版”中存在的问题,把公式做成灵活可配,就是一件有意义的事情。

 

零、公式计算描述

       此次需求,客户选择金融产品后,根据所选择的“自有”、“浦发”、“微众”三种类型资金来源,来执行试算操作,每执行一次试算,需要44个公式执行计算操作。

 

一、先看老方式:

     最初的公式,都维护在代码中,虽然不够灵活,但是代码组织结构写的很棒:

     1.接口统一定义所有公式的名称、方便各个子类在计算时,名称混乱;

     2.父类实现一些不常变化的公式方法,定义计算所有公式时,每个公式的先后执行顺序;

     3.根据业务标准(新车、旧车;资金来源;产品类型)等,定义第三层公式实现类(子类按照业务标准定义并继承)

     4.对外暴露Adapter,外界通过这个类,来调用不同业务标准下的公式计算。

     我还是很喜欢这块当时的设计,值得学习。(因代码不对外,只说一下思路)

      

二、再看新方式:

     1.目的,实现公式改动时“后台可配,无需编码”。

     2.整体思路:

 

     3.几点实现细节

      0)界面设计

       如图,每种资金来源占用一个tab页,公式类型维护在枚举,公式内容及说明存放在mysql和redis中,每次更新通过“cache aside pattern”策略,同步更新redis和mysql,且记录每次公式的修改,便于历史记录查看,及错误追踪。

       关于权限,在“菜单”入口处,设置只有“特定人员”可以看到并进入此页面。

 

      1)aviator表达式引擎

       使用aviator过程,遵循预先编译表达式的原则,从redis获取到公式集,根据公式id获取内容后,先编译,后执行:

/**
 * Double类型,算式执行
 * @param param 公式执行时的入参,线程安全
 * @param express string类型的表达式内容
 * @return 
 * @throws ExpressionRuntimeException
 */
public static Double doExecute(Integer key, String express,ConcurrentHashMap<String , Object> param) throws              CompileExpressionErrorException,ExpressionRuntimeException {
    //1.编译表达式
    Expression expression  = AviatorEvaluator.compile(express);
    if(expression ==null){
        throw new CompileExpressionErrorException(String.format("[%s]解析失败", ProdFormulaEnum.getNameByIndex(key)));
    }
    //2.执行计算,入参提前存放到ConcurrentHashMap中
    Object result = expression.execute(param);
    return (Double) result;
}

      2)内置函数支持

      对于公式中的三元运算、幂运算,参考第一篇中的讲解,注意,aviator不支持“[]”作为优先级的符号。

 

      3)redis设计

      起初AviatorUtil中,加入了本地缓存,代码如下:

/**
 * 缓存编译后的表达式,无需再次编译
 */
ConcurrentHashMap<String , Expression> expressMap = new ConcurrentHashMap<String , Expression>();

/**
 * 算式执行,返回类型为Double
 * @param param
 * @param express
 * @return
 * @throws ExpressionRuntimeException
 */
public Double doExecute(HashMap<String , Object> param, String express)throws CompileExpressionErrorException,ExpressionRuntimeException {
    Expression expression = null;
    //设计思路,编译过的表达式,无需再次编译
    synchronized (lock) {
        expression = expressMap.get(express);
        if (expression == null) {
            expression = AviatorEvaluator.compile(express);
            expressMap.put(express, expression);
        }

    }
    if(expression ==null){
        throw new CompileExpressionErrorException(String.format("[%s]解析失败",express));
    }
    Object result = expression.execute(param);
    return (Double) result;
}

        页面配置修改后,清空此“expressMap”内容,但此项目线上部署多台机器,公式一旦修改,nginx也只能清空调一台机子中的缓存,于是,本地缓存编译结果方式,不予考虑。

       最终,通过“cache aside pattern”,把修改后的公式优先同步到redis,每次试算拉取redis中最新的表达式内容,再执行试算。并发情况下,redis压力正在测试。

 

     遇到问题:

     1.调试不便

     复杂公式计算失败,很难定位到问题,建议打开TRACE模式

public static Double doExecute2(Integer key, String express,HashMap<String , Object> param)throws CompileExpressionErrorException,ExpressionRuntimeException {
	//如下所示
    AviatorEvaluator.setOption(Options.TRACE_EVAL, true);
	Expression expression  = AviatorEvaluator.compile(express);
	if(expression ==null){
		throw new CompileExpressionErrorException(String.format("[%s]解析失败", ProdFormulaEnum.getNameByIndex(key)));
	}
	Object result = expression.execute(param);
	return (Double) result;
}

         每一个公式,每一步的执行都非常清晰:

[Aviator TRACE]          <JavaType, 90000.0, Double> -sub <JavaType, 55.0, Double> => <Double, 89945.0>
[Aviator TRACE]          <JavaType, 1, Integer> / <Long, 12> => <Long, 0>
[Aviator TRACE]          <Long, 0> * <JavaType, 1, Integer> => <Long, 0>
[Aviator TRACE]          <Double, 89945.0> * <Long, 0> => <Double, 0.0>
[Aviator TRACE]          <JavaType, 1, Integer> / <Long, 12> => <Long, 0>
[Aviator TRACE]          <Long, 0> * <JavaType, 1, Integer> => <Long, 0>
[Aviator TRACE]          <Long, 1> + <Long, 0> => <Long, 1>
[Aviator TRACE] Func   : math.pow(<Long, 1>,<JavaType, 3, Integer>)
[Aviator TRACE]          <Double, 1.0> -sub <Long, 1> => <Double, 0.0>
[Aviator TRACE]          <Double, 0.0> / <Double, 0.0> => <Double, NaN>
[Aviator TRACE]          <JavaType, 90000.0, Double> -sub <JavaType, 55.0, Double> => <Double, 89945.0>
[Aviator TRACE]          <JavaType, 1, Integer> / <Long, 12> => <Long, 0>
[Aviator TRACE]          <Long, 0> * <JavaType, 1, Integer> => <Long, 0>
[Aviator TRACE]          <Double, 89945.0> * <Long, 0> => <Double, 0.0>
[Aviator TRACE]          <Double, NaN> + <Double, 0.0> => <Double, NaN>
[Aviator TRACE] Result : NaN

      2.整型返回值

Long result = (Long) AviatorEvaluator.execute("1+2+3");

        如上,Aviator的数值类型仅支持Long和Double, 任何整数都将转换成Long, 任何浮点数都将转换为Double, 包括用户传入的变量数值。适配整型结果,2中方案:

       1)统一将结果处理为BigDecimal类型

AviatorEvaluator.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true);

       2)AviatorUtil,重写一个返回值为Long的execute方法,我采用此方式。

 

       3.线程安全

       疑惑“多个线程同时调用 AviatorEvaluator.execute() 会存在多线程安全问题吗”,看了下execute方法源码,且在issue中,作者说明了“AviatorEvaluator.execute() 本身是线程安全的。只要你的表达式执行逻辑是线程安全的,传入的 env 是线程安全的,那就没有问题”。

       https://github.com/killme2008/aviator/issues/91(参考这个issue)

     4.能够撑住多大试算并发量,在测试过程中不断试验。

 

三、总结

      1.新老方式都不错,开关可以随意切换,算是个容灾策略。

      2.未来试算的方向应该是走动态配置,过渡过程需要维护2套代码。

      3.aviator的一个缺点,公式中如果有逻辑,且不能通过三元运算转义,这时,配置中的公式就受限。

 

      欢迎大佬们,指正,互相交流。

 

      That's all.

      2019年3月16日17:27:20 (周六)

 

 

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值