一、规则引擎的背景
业务系统在应用过程中,常常包含着要处理"复杂、多变"的部分,这部分往往是"业务规则"或者是"数据的处理逻辑"。因此这部分的动态规则的问题,往往需要可配置,并对系统性能和热部署有一定的要求。从开发与业务的视角主要突出以下的一些问题:
1.1从开发人员视角来看
1)逻辑复杂,要使用大量if-else来实现,或者使用设计模式。但过于复杂的规则逻辑,使用设计模式也往往是存在大量并且关系复杂的类,导致代码难于维护,对新加入的同学极不友好。
2)变更时需要从头梳理逻辑,在适当的地方进行if...else...代码逻辑调整,耗费大量时间进行梳理。
3)开发周期较长,当需求发生变更时,需要研发人员安排开发周期上线,对于当下快速变化的业务,传统的开发工作方式显得捉襟见肘。
1.2从业务人员视角来看
1)业务人员期望友好的管理界面,不需要专业的开发技能就能够完成规则的管理、发布。
2)期望能够实现热部署,由业务人员配置好之后即配即用。
3)减少业务规则对开发人员的依赖。
4)降低需求变动的时间成本,快速验证发布。
二、规则引擎介绍
2.1什么是规则引擎
规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。
将规则引擎想象成为一个以数据和规则作为输入的系统。它将这些规则应用于数据,并根据规则定义为我们提供输出。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
2.2 规则引擎的核心概念
Rule:由条件和行动构成的推理语句,一般表示为IF <conditions> THEN <actions>, Rule表达逻辑。一个规则的IF部分称为LHS,THEN部分称为RHS。
Condition:(Left Hand Side)LHS,即条件分支逻辑
Action:(Right Hand Side)RHS,即执行逻辑
Fact:用户输入的事实对象,作为决策因子使用
RulesEngine:引擎执行器,一般为推理引擎。Rules使用LHS与事实进行模式匹配。当匹配被找到,Rules会执行RHS即执行逻辑,同时actions经常会改变facts的状态,来推理出期望的结果。
Output:结果对象,规则处理完毕后的结果。
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
三、规则引擎适用场景
当一个业务从小到大不断壮大的过程中,规则无疑是需要经常变动的,而且往往需要快速的上线,来适应市场的变化。这就要求程序猿们要不断的付出更多的努力来尽快满足业务的需要。而在较为复杂的业务场景下,程序猿们对需求的理解也经常与产品的需求不能达到完全一致。在以下几种情况时,更适合于引入规则引擎:
四、EasyRules引擎的应用
4.1 项目遇到的业务场景
在实际项目中,消息调度系统要求需要根据消息的场景、内容、通道、事件进行消息策略选择。针对于不同的消息场景使用不同的策略、通道来对用户进行触达是至关重要的。试想一下当关键消息没有最终触达到用户/商家的话,对交易双方都是不少的损失。反之一旦部分消息频繁的打扰用户,也往往事倍功半,导致大量用户失去对App的耐心,造成用户流失。调度策略需要针对情景、用户行为、事件针对用户进行不同通道、策略的触达是调度系统的重要意义。
然而为了满足大量复杂因子在业务规则中的决策逻辑,并且要能够支持规则不断的调整与扩展,使用硬编码的方式编写到系统当中又是一件需要耗费大量精力的事情。此时在部门老贾的建议下,规则引擎进入了大家的视野。能够将业务规则从系统中抽离出来,使用引擎来进行决策,这不正是我们需要的东西嘛。相较于Drools、URule等这种完善的规则管理系统,EasyRules作为一个基于java轻量级的规则引擎,学习成本更低、适用性更强,故而选择使用EasyRules实践规则引擎。
4.2 通过业务建模来解决调度策略的复杂问题
1)系统往往仅需要决策结果来进行业务处理,并不需要推理决策过程的中间结果。
2)业务决策受多个事实因素的影响
3)业务决策和影响的事实因素是已知的,也就是说在事实因素确认的前提下,只能推理出唯一的决策decision=rules(fact1, fact2, fact3)
4.3来看下小伙伴们的第一个版本,来领略下规则引擎的世界
1)既然是规则引擎,先来看看基于注解式声明规则是怎么一个简单规则
注解式声明Rule |
@Rule (name = "repeatRule" , description = "重复规则对消息内容进行去重" , priority = 100 )
public class RepeatRule {
@Condition
public boolean evaluate( @Fact ( "deliveredMessages" ) Set<String> deliveredMessages,
@Fact ( "message" ) String message) {
// 检查去重
return deliveredMessages.contains(message);
}
@Action (order = 0 )
public void execute(Facts facts){
// 丢弃消息
}
@Action (order = 1 )
public void breakRules(Facts facts) {
facts.put( "break" , true );
facts.put( "reason" , "repeat limit" );
}
}
|
2)复合规则一定是由多个简单规则来实现的,对于通道规则而言单通道下发与多通道下发也是依赖于不同场景。每个单一通道其实我们可以看作是一个规则,将多个规则嵌套在一个复合规则里来实现。而复合规则可能遵循着不同的实现来决策最终结果。EasyRules实现了ActivationRuleGroup、ConditionalRuleGroup、UnitRuleGroup3中规则,分别是优先激活规则组、条件规则组、单元规则组。我们实现ChannelRule继承了ActivationRuleGroup的复合规则组,ChannelPushRule和ChannelLetterRule等(其他通道规则省略)为组成的简单规则。
复合规则Rule |
public class ChannelRule extends ActivationRuleGroup {
public ChannelRule(Object... rules) {
super ( "channelRule" );
setPriority( 200 );
addRule(rules);
}
public ChannelRule(){
super ( "channelRule" );
setPriority( 200 );
}
|
简单Push规则Rule |
@Rule (name = "push channel" , description = "PUSH通道" , priority = 100 )
public class ChannelPushRule {
@Condition
public boolean evaluate( @Fact ( "deliveredSet" ) Set deliveredSet,
@Fact ( "msgId" ) String msgId) {
return !deliveredSet.contains(msgId);
}
@Action
public void execute(Facts facts) {
facts.put( "channel" , PUSH);
}
|
简单私信规则Rule |
@Rule (name = "letter channel" , description = "私信通道" , priority = 900 )
public class ChannelLetterRule {
@Condition
public boolean evaluate( @Fact ( "deliveredSet" ) Set deliveredSet,
@Fact ( "msgId" ) String msgId){
return !deliveredSet.contains(msgId);
}
@Action
public void execute(Facts facts) {
facts.put( "channel" , LETTER);
}
}
|
3)完成了规则的定义就相当于完成了一半的开发了,毕竟在规则引擎里一切皆为规则,接下来我们来看看如何执行引擎的加载。
fire规则引擎 |
public class RuleDispatcherTest {
@Test
public void doDispatcher() {
// 去重简单规则
RepeatRule repeatRule = new RepeatRule();
// 通道复合规则的构造
ChannelRule channelRule = new ChannelRule();
channelRule.addRule( new ChannelPushRule());
channelRule.addRule( new ChannelPopupRule());
channelRule.addRule( new ChannelLetterRule());
// 聚合规则
AggregationRule aggregationRule = new AggregationRule();
// 频控规则
FrequencyRule frequencyRule = new FrequencyRule();
Rules rules = new Rules(repeatRule, channelRule, aggregationRule, frequencyRule);
// define facts
Facts facts = new Facts();
facts.put( "msgId" , "123456" );
facts.put( "deliveredSet" , new TreeSet<>());
facts.put( "msgType" , "interaction" );
DefaultRulesEngine engine = new DefaultRulesEngine();
engine.registerRuleListener( new MyRulesListner());
engine.fire(rules, facts);
log.info( "engine fire over , facts=>{}" , facts.toString());
}
}
|
4)上面的代码同时引用了RuleListener来监听规则在执行过程中的行为,观察到结果在执行通道规则时命中返回了相应的结果。
执行结果 |
17 : 08 : 19.554 [main] INFO - before rule, name=>repeatRule, evaluation result=> false
17 : 08 : 19.556 [main] INFO - before rule, name=>channelRule, evaluation result=> true
17 : 08 : 19.557 [main] INFO - success rule, name=>channelRule, execute result=>{}
17 : 08 : 19.557 [main] INFO - before rule, name=>aggregationRule, evaluation result=> false
17 : 08 : 19.557 [main] INFO - before rule, name=>frequencyRule, evaluation result=> false
17 : 10 : 33.851 [main] INFO - engine fire over , facts=>[Fact{name= 'result' , value={}},Fact{name= 'msgType' , value=interaction},Fact{name= 'channel' , value=PUSH},Fact{name= 'msgId' , value= 123456 },Fact{name= 'deliveredSet' , value=[]}]
|
如果只是到了这里,也仅仅只是代码层面的优化。还是无法显示出规则引擎备受推崇的原因,我们来看看下面的版本又能给我们带来什么样的惊喜
4.4 基于表达式语言实现第二个版本的规则引擎
1)一切皆规则,那还是从规则说起,这里我们首先要简单介绍下MVEL表达式语言,MVFLEX Expression Language,是Java平台的动态/静态混合类型的运行时可嵌入表达式语言。它的很多特性使他能够很好的将一些逻辑公开给最终用户和程序猿们,其中包括属性表达、值检测、集合表达、流程控制、投影、交集、函数定义等功能。我们来看下如何通过MVEL字符串表达式来进行业务规则的解藕。
MVEL实现规则定义 |
public class MVELRuleDispatcherTest {
public Rules buildMvelRules() {
MVELRule pushRule = new MVELRule()
.name( "mvelPUSHRule" )
.description( "mvel push rule" )
.priority( 100 )
.when( "!deliveredSet.contains(msgId);" )
.then( "result.put('channel', 'PUSH');" );
MVELRule popupRule = new MVELRule()
.name( "mvelPopupRule" )
.description( "mvel popup rule" )
.priority( 200 )
.when( "!deliveredSet.contains(msgId)" )
.then( "result.put('channel', 'POPUP')" );
MVELRule letterRule = new MVELRule()
.name( "mvelLetterRule" )
.description( "mvel letter rule" )
.priority( 300 )
.when( "!deliveredSet.contains(msgId)" )
.then( "result.put('channel', 'LETTER')" );
ActivationRuleGroup channelRuleGroup = new ActivationRuleGroup( "channel rule" );
channelRuleGroup.addRule(pushRule);
channelRuleGroup.addRule(popupRule);
channelRuleGroup.addRule(letterRule);
channelRuleGroup.setPriority( 200 );
Rule repeatRule = new MVELRule()
.name( "repeat rule" )
.priority( 100 )
.when( "deliveredSet.contains(msgId)" )
.then( "result.put('break', true); result.put('reason', 'repeat limit')" );
Rule aggregationRule = new MVELRule()
.name( "aggregation rule" )
.priority( 300 )
.when( "result['break'] != true && msgType == 'aggregation'" )
.then( "def aggregate(){ System.out.println('aggregate multi messages.'); return new java.util.HashMap();}; java.util.Map oneMsg = aggregate(); result.put('aggregationMsg', oneMsg),; result.put('aggregationDetails', [msgId, msgId]);" );
Rule frequencyRule = new MVELRule()
.name( "frequency rule" )
.priority( 400 )
.when( "result['break'] != true && deliveredSet.contains(msgId)" )
.then( "def doDelay(msgId){}; doDelay(msgId); result.put('break', true); result.put('reason', 'hit rate rule');" );
return new Rules(repeatRule, channelRuleGroup, aggregationRule, frequencyRule);
}
}
|
2)在上面的代码中,我们看到condition与action均使用MVEL字符串表达式来定义了一个个完整Rule,那么规则的定义其实可以单独抽取按照mvel表达式语法来管理,既然可以将规则从系统剥离出来,我们来看下怎么加载解藕的规则代码。easy-rules已经支持了快速简单的从yml、json文件中加载Rule描述文件。以下间断的代码能够支持从规则描述文件中加载规则,并通过引擎进行规则与事实的推理。
引擎加载规则库 |
public class MVELRuleDispatcherTest {
@Test
public void doDispatcherFromJsonReader() throws Exception {
MVELRuleFactory factory = new MVELRuleFactory( new JsonRuleDefinitionReader());
File file = new File( "src/test/resources/rules/rules.json" );
InputStream in = new FileInputStream(file);
Reader reader = new InputStreamReader(in);
Rules rules = factory.createRules(reader);
assertThat(rules).hasSize( 4 );
Facts facts = new Facts();
facts.put( "msgId" , "123456" );
Set set = new TreeSet<>();
facts.put( "deliveredSet" , set);
facts.put( "msgType" , "interaction" );
facts.put( "msgType" , "aggregation" );
facts.put( "list" , new ArrayList<>());
DefaultRulesEngine engine = new DefaultRulesEngine();
engine.registerRuleListener( new MyRulesListner());
engine.fire(rules, facts);
log.info( "fire over, facts =>{}" , facts.asMap());
}
}
|
规则已经完全从系统中抽离出来,我们不仅仅可以存储在文件中,我们可以单独的存储在数据库或者内存当中进行管理,不再需要上线变更升级了。现在看来规则变得越来越清晰、干净了,下回有新同学参加到项目中来可以更快速的了解业务,把复杂的规则单独讲清楚就可以啦。
五、总结与展望
规则引擎在各个应用系统中存在这应用价值,能够给业务变化、发展带来较大的助力。本次在项目中引入规则引擎来代替传统的硬编码方式,提供了更好的解决方式。这种将规则与业务逻辑抽离的方式更便于规则的统筹管理,并且热加载功能使得项目能够在更短的周期内进行快速迭代。目前还依旧存在着一些问题不足,比如规则不能完全脱离开发人员,需要屏蔽掉对业务人员的技术壁垒;规则没有版本控制,在规则在验证阶段失败时需要修改为原来的逻辑;需要更有好的界面可以拖拽实现规则的编排;引擎的预编译进行性能方面的优化等等。
初步确认了规则引擎在后续项目中的应用定位,我们致力于在更多的项目中能够通过这种轻量级的规则引擎带来更大的效能提升。计划未来从以下几个方面进行规则引擎的完善:
1)解藕知识与业务逻辑的耦合性,单独维护知识库来进行知识与版本的管理
2)更优的表达式语言或者动态编译语言来提高规则引擎的性能。
3)通过使用预编译、缓存、线程池技术来优化管理引擎的执行性能。
4)支持灰度版本管理,在线验证,快速发布。缩减需求变动的周期时间。
5)屏蔽语言壁垒,能够让运营、产品支持在线规则配置、发布是最终要实现的重要目标。做到无需研发接入,实时发布。
6)开发规则集、规则表、规则树、评分卡、规则流来完善规则管理
' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
文章来自一点资讯运营中台部