多维复杂业务规则典型场景下轻量级规则引擎的通用设计模式

规则引擎有非常多的的开源成熟实现,我们这里不介绍具体的Drools等开源框架的技术实现,而是看一下在具体场景下,具体的应用模式,以及使用注意点。

本文基于自己最近的一个应用场景需要,实践过程中遇到的问题,以及综合了网上几篇高质量文章,参考见文末。

1

规则引擎的价值

价值

一言以蔽之,规则引擎的价值,就是为了解决大量业务规则,灵活变化的配置问题。

复杂点说,规则引擎实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。

简单点说,我们开发程序的一个重要原则就是“封装变化”,对于某些场景来说,流程是确定的,但是决策条件可能是复杂多变的。

举个例子,商业银行一般有总行、分行、支行三个层级。

一些业务场景,需要按照总行给定一个兜底的规则,但是每个分行可能根据各地的监管机构有不同的要求,一般是更严格需要个性化的配置,某些免税区的支行呢,又可能对业务有其它的要求。

我们当然不能通过写大量的if-else代码,或者使用了某些策略模式等去开发,因为改动了代码就需要上线,支撑业务变化的效率永远跟不上节奏,绝不是我们能接受的。

规则引擎应运而生,允许在不重新启动系统或部署新的可执行代码的情况下更改规则。

另外,规则引擎,一般通过声明式编程方式,允许你描述做什么而不是如何去做,更加快了开发速度。

应用原理

Java社区制定了Java规则引擎API(JSR-94)规范,javax.rules包定义,是访问规则引擎的标准企业级API。规则管理API(the rules administration API)和运行时客户API(the Runtime client API)。

⑴规则管理API:

  规则管理API在javax.rules.admin中定义,包括装载规则以及与规则对应的动作(执行集 execution sets)以及实例化规则引擎。规则可以从外部资源中装载,比如URI,Input streams,XML streams和readers等等。同时管理API提供了注册和取消注册执行集以及对执行集进行维护的机制。使用admin包定义规则有助于对客户访问运行规则进行控制管理,它通过在执行集上定义许可权使得未经授权的用户无法访问受控规则。

  管理API使用类RuleServiceProvider来获得规则管理(RuleAdministrator)接口的实例。规则管理接口提供方法注册和取消注册执行集。规则管理器(RuleAdministrator)提供了本地和远程的RuleExecutionSetProvider。在前面已提及,RuleExecutionSetProvider负责创建规则执行集。规则执行集可以从如XML streams,input streams等来源中创建。这些数据来源及其内容经汇集和序列化后传送到远程的运行规则引擎的服务器上。大多数应用程序中,远程规则引擎或远程规则数据来源的情况并不多见。为了避免这些情况中的网络开销,API规定了可以从运行在同一JVM中规则库中读取数据的本地RuleExecutionSetProvider。

  规则执行集接口除了拥有能够获得有关规则执行集的方法,还有能够检索在规则执行集中定义的所有规则对象。这使得客户能够知道规则集中的规则对象并且按照自己需要来使用它们。

⑵运行时客户API:

  运行时API定义在javax.rules包中,为规则引擎用户运行规则获得结果提供了类和方法。运行时客户只能访问那些使用规则管理API注册过的规则,运行时API帮助用户获得规则对话并且在这个对话中执行规则。

  运行时API提供了对厂商规则引擎API实现的类似于JDBC的访问方法。规则引擎厂商通过类RuleServiceProvider(类RuleServiceProvider提供了对具体规则引擎实现的运行时和管理API的访问)将其规则引擎实现提供给客户,并获得RuleServiceProvider唯一标识规则引擎的URL。

  URL推荐标准用法是使用类似“com.mycompany.myrulesengine.rules.RuleServiceProvider”这样的Internet域名空间,这将有助于访问URL的唯一性。类RuleServiceProvider内部实现了规则管理和运行时访问所需的接口。所有的RuleServiceProvider要想被客户所访问都必须用RuleServiceProviderManager进行注册。注册方式类似于JDBC API的DriverManager和Driver。

  运行时接口是运行时API的关键部分。运行时接口提供了用于创建规则会话(RuleSession)的方法,规则会话如前所述是用来运行规则的。运行时API同时也提供了访问在service provider注册过的所有规则执行集(RuleExecutionSets)。规则会话接口定义了客户使用的会话的类型,客户根据自己运行规则的方式可以选择使用有状态会话或者无状态会话。

  无状态会话的工作方式就像一个无状态会话bean。客户可以发送单个输入对象或一列对象来获得输出对象。当客户需要一个与规则引擎间的专用会话时,有状态会话就很有用。输入的对象通过addObject() 方法可以加入到会话当中。同一个会话当中可以加入多个对象。对话中已有对象可以通过使用updateObject()方法得到更新。只要客户与规则引擎间的会话依然存在,会话中的对象就不会丢失。

  RuleExecutionSetMetaData接口提供给客户让其查找规则执行集的元数据(metadata)。元数据通过规则会话接口(RuleSession Interface)提供给用户。

2

技术选型需要考虑的维度

我们在进行技术选型的时候,需要考虑如下几个维度:

业务复杂度

也就是说,规则类型有多少,是否要非常复杂的规则关系支持?

这个规则的使用频率有多少,是否值得投入非常完善的框架去实现,ROI(投入产出比)怎么样?

如果只是某些不常用的功能有需求,那么没有必要去选择非常重,学习成本高的框架,例如DRools。

业务复杂度最终体现在规则复杂度上:

规则本质是一个函数,由n个输入、1个输出和函数计算逻辑3部分组成。

即:y = f(x1, x2, …, xn)

如果执行单一的规则判断结果,可以使用Groovy等轻量级脚本语言,但是如果规则之间还有关联关系,业务规则不是完全独立的,那么就需要规则引擎在正向推理的时候,需要一套高效的模式匹配算法支撑(例如Drools的Rete算法),这就需要Drools这种专业框架了。

技术复杂度

受限于项目交付压力,科技人员也往往需要考虑技术本身的复杂度。

例如框架的学习成本、部署成本、集成成本等。

如果我们做的是业务应用系统,而不是为了做一个规则引擎的企业级平台服务,很显然能支撑业务发展的前提下,越轻量级越好。

当然,前提条件还是在充分评估扩展性上。

例如对于Drools还是Groovy的选型,有可能几种情况选择了Groovy:

  • drools相对来说有点重,而且它的规则语言不管对于开发还是运营来说都有学习成本

  • drools使用起来没有groovy脚本灵活。groovy可以和spring完美结合,并且可以自定义各种组件实现插件化开发。

  • 当规则集变得复杂起来时,使用drools管理起来有点力不从心。

使用便利度

一般情况下应用了规则引擎后,需要把规则的配置处理交给业务某个前端页面去做。

那么我们在进行处理的时候,就需要考虑,是否能够形成前端易用的配置?

例如,如果选择了Groovy语言,那前端页面绝不能直接暴露出来Groovy的语法,而是要用业务语言来描述,这就涉及到了前端规则配置的设置。

可能遇到的坑

例如groovy脚本,当JVM中运行的Groovy脚本存在大量并发时,如果按照默认的策略,每次运行都会重新编译脚本,调用类加载器进行类加载。不断重新编译脚本会增加JVM内存中的CodeCache和Metaspace,引发内存泄露,最后导致Metaspace内存溢出;类加载过程中存在同步,多线程进行类加载会造成大量线程阻塞,那么效率问题就显而易见了。为了解决性能问题,最好的策略是对编译、加载后的Groovy脚本进行缓存,避免重复处理,可以通过计算脚本的MD5值来生成键值对进行缓存。下面我们带着以上结论来探讨。

可能的解决方案:

  • 对于 parseClass 后生成的 Class 对象进行缓存,key 为 Groovy脚本的md5值,并且在配置端修改配置后可进行缓存刷新。这样做的好处有两点:(1)解决Metaspace爆满的问题;(2)因为不需要在运行时编译加载,所以可以加快脚本执行的速度。

  • GroovyClassLoader的使用用参考Tomcat的ClassLoader体系,有限个GroovyClassLoader实例常驻内存,增加处理的吞吐量。

  • 脚本静态化:Groovy脚本里面尽量都用Java静态类型,可以减少Groovy动态类型检查等,提高编译和加载Groovy脚本的效率。

安全性

使用了规则引擎,相当于把部分可执行代码暴露出去了,这样必须考虑安全性问题。

例如Groovy会自动引入java.util,java.lang包,方便用户调用,但同时也增加了系统的风险。为了防止用户调用System.exit或Runtime等方法导致系统宕机,以及自定义的Groovy片段代码执行死循环或调用资源超时等问题,Groovy提供了SecureASTCustomizer安全管理者和SandboxTransformer沙盒环境。

虽然SecureASTCustomizer可以对脚本做一定程度的安全限制,也可以规范流程进一步强化,但是对于脚本的编写仍然存在较大的安全风险,很容易造成cpu暴涨、疯狂占用磁盘空间等严重影响系统运行的问题。所以需要一些被动安全手段,比如采用线程池隔离,对脚本执行进行有效的实时监控、统计和封装,或者是手动强杀执行脚本的线程。

3

规则引擎分类及技术选型考虑维度

规则引擎分类

  1. 完整规则引擎。
    这类引擎功能齐全,有前端配置能力,但是非常重,例如典型的DRools。

  2. 基于JVM脚本语言。
    这种典型的如Groovy、AViator。
    Aviator表达式求值引擎,主要用于各种表达式的动态求值。在美团内部基本大部分使用规则引擎的场景比如风控,数据规则等等都选择了aviator这个轻量级的语言作为规则引擎。
    开源的风控引擎radar就是使用的Groovy去实现的。

    Aviator的处理框架例子:如果我们想实现订单金额大于100元并且用户属于vip这个规则在aviator中应该怎么做呢?

    public static void main(String[] args) {
    //首先构造参数
    Map<String, Object> env = new HashMap<String, Object>();
    env.put("orderAmount", 101);
    env.put("vip", true);

    // 执行表达式逻辑
    Boolean result = (Boolean) AviatorEvaluator.execute("orderAmount > 100 && vip", env);
    System.out.println(result);
    }
  3. 基于Java代码。
    例如easyRules,这种引擎不需要额外的学习成本。它的诞生启发自有Martin Fowler 一篇名为 “Should I use a Rules Engine?” 文章。

例如:

@Rule(priority = 1)
public class FuyanRule {
@Condition
public boolean isFuyan(@Fact("biz") String biz) {
return biz == "用途";
}

@Action
public void process(Facts facts) {
String tradeNo = facts.get("tradeNo");
facts.put("tradeNo", "tutor" + tradeNo);
}
}

@Rule注解中定义priority代表我们的if/else优先级, @Condition就是我们的条件判断,如果属于则进入条件判断,@Action是我们匹配之后的动作。

4

轻量级通用设计模式

假如使用了Groovy脚本,那么在考虑了内存溢出、安全等方面的内容后,需要看一下应用层的设计模型了。

简单来说,有两张表:

  1. 规则表。包含规则id、规则描述、规则内容(可能是整个Groovy脚本)等

  2. 业务参数表。包含具体的业务参数全集以及配置维度。

  3. 关联关系表。即,某个业务场景参数下,执行那个规则。

剩下的就交由Java调用Groovy引擎进行处理了。

参考

  1. https://zhuanlan.zhihu.com/p/358983669

  2. https://blog.csdn.net/NarutoConanKing/article/details/87431397

  3. https://tech.meituan.com/2017/06/09/maze-framework.html

  4. https://zhuanlan.zhihu.com/p/395699884

  5. https://www.cnblogs.com/ityml/p/15993391.html


原文始发于微信公众号(架构突围):多维复杂业务规则典型场景下轻量级规则引擎的通用设计模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/170092.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!