1.什么是规则引擎和命令式编程
让我们先来看一个有趣的猜数字小游戏:玩家需要猜测一个1到100之间的随机数字。每次猜测后,系统会提示玩家所猜的数字是大于还是小于随机数,玩家需要根据提示继续猜测,直到猜中为止。
// 生成一个1到100之间的随机整数
secret = random(1, 100)
// 初始化猜测次数为0
guesses = 0
// 循环猜数字
while true do
// 读取用户输入的整数
guess = input("Guess a number between 1 and 100: ")
guesses = guesses + 1
// 判断猜测结果
if guess < secret then
print("Too low, try again.")
else if guess > secret then
print("Too high, try again.")
else
print("you guessed it in", guesses, "guesses!")
break
end if
end while
使用while循环来保持程序持续运行,用以判断数字大小并记录猜测次数。这是我们常采用的命令式编程方式:明确地指定每个步骤的执行顺序和详细的操作细节,例如变量的赋值、条件判断、循环控制等。
再来看下规则引擎编程方式:
// 定义规则1
rule "Guess a number"
when
$guess: Integer()
$secret: Integer(intValue > $guess)
then
System.out.println("Too low, try again.");
end
//定义规则2
rule "Guess a number"
when
$guess: Integer()
$secret: Integer(intValue < $guess)
then
System.out.println("Too high, try again.");
end
//定义规则3
rule "Guess a number"
when
$guess: Integer()
$secret: Integer(intValue == $guess)
then
System.out.println("you guessed it!");
end
上述代码定义了3条规则,每条规则都包含执行条件(when语句)和动作(then语句)。其中,规则1指定:当输入的数字小于initValue时,应打印 “Too low, try again.”。规则引擎编程方式是:将具体的代码逻辑抽象为对应的业务规则,并通过这些规则的定义和执行来实现。
规则引擎编程价值
当我们能够将业务逻辑代码抽象为相应的业务规则时,业务人员就可以通过修改规则的条件和动作来快速迭代业务逻辑。这正是规则引擎的第一个价值:业务具有高度的可扩展性。
规则引擎的另一个价值是:项目具有高度的可维护性。与上述命令式编程方式实现的小游戏代码相比,多个if-else语句不仅增加了代码的复杂度和维护成本,还易导致代码的可读性和可维护性降低。而规则引擎方式使业务流程更加清晰和直观,降低应用程序的耦合度,并在一定程度上实现业务与技术的分离。
总之,规则引擎是一种更高级的条件判断手段。它通过规则的方式来决定行为,使用简单的规则语言来表达复杂的业务逻辑,并具有更好的业务可扩展性和项目可维护性。
2.规则引擎在转转钱包的应用
转转钱包是一个有温度的金融钱包。在这里,可以参与免息分期购物活动,使用安全快捷的小额借贷服务,甚至可以1元租用高端手机。欢迎大家来体验和使用。
转转钱包
在最近对“我的钱包”进行的改版中,业务同学提出需求:根据各个用户当前的业务状态展示相应的分期、借钱以及租赁的卡片内容和页面跳转路径。
如上图所示的需求中,借钱卡片包含7种场景,分期卡片包含5种场景,手机租赁包含3种场景。如果按照常规的命令式编程方式:
- 代码中将包含大量的if-else语句,可维护性会变差
- 一旦业务方想要调整某状态下的交互行为,需要修改代码并重新发版
规则引擎在执行前,需要计算所有用户的业务状态,而在某些场景下,命令式编程可能无需计算所有业务状态就可以得出结果,这可以在一定程度上提高性能。在权衡利弊后,我们决定在转转钱包中采用规则引擎,因为其优点远大于缺点。
规则建模
在使用规则引擎之前,有一个关键点需要充分考量:是否可以构建一个良好的规则模型。一个好的规则模型可以使规则系统更易于理解、维护和扩展。比如上文提到的借钱卡片状态,我们可以抽象出以下规则:账户是否停用、是否新户、是否可以申请贷款、是否有额度。找到这些规则条件后,我们可以反过来检查这些规则是否可以覆盖所有的状态描述,以避免业务场景有遗漏。简言之,我们要找出业务逻辑中共性的规则条件,然后使用这些条件来倒推校验业务逻辑的完整性。
选择引擎组件
你可以自己构建一个简单的规则引擎。只需要创建一组带有条件和操作的对象,将它们存储在合适的集合中,然后遍历这些对象来评估条件和执行操作。当然,我们没有必要重新造轮子,市面上已经有几个常用的规则引擎组件,例如:drools、easy-rules、aviator和liteFlow等。大家可以根据自己的业务场景选择合适的组件。转转钱包选择了easy-rules,因为在满足业务需求的基础上,它短小强悍。
整体设计
如下图所示,我们将规则配置在Apollo中以实现动态调整。高效地计算每个用户的分期、借钱和租赁状态,再将规则集和相关事实输入到规则引擎中,最后得到各卡片的结果状态。在此过程中,可能会有以下疑问:该规则引擎的执行效率如何?它又是如何评估规则的?带着这些疑问,让我们来看看规则引擎的源码实现。
3.EasyRules性能分析
本节将通过阅读easy-rules规则引擎中与规则评估和执行相关的源码,来了解其效率水平
支持MVEL和Spel表达式
规则评估
通过查看org.jeasy.rules.core.DefaultRulesEngine#fire方法,我们进入到doFire() 方法里
void doFire(Rules rules, Facts facts) {
for (Rule rule : rules) { //遍历规则
boolean evaluationResult = false;
evaluationResult = rule.evaluate(facts); //评估规则条件是否成立
if (evaluationResult) {
rule.execute(facts); //如果成立,执行规则的动作
}
}
}
上面代码只保留了主要逻辑,规则评估通过for循环遍历规则集,逐一评估每个规则的条件是否满足,如果条件满足则执行相应的动作。但是,如果您的规则量非常大,此规则引擎组件可能不是最佳选择。这时可以考虑使用高效的Rete规则匹配算法。Rete算法巧妙地利用了规则之间的关联关系,构建一个高效的规则匹配网络。当有新事实进入时,它可以高效地匹配该事实与已有规则的匹配情况。
规则执行
protected Rule createSimpleRule(RuleDefinition ruleDefinition) {
MVELRule mvelRule = new MVELRule(parserContext)
.name(ruleDefinition.getName())
.priority(ruleDefinition.getPriority())
.when(ruleDefinition.getCondition()); //步骤1
for (String action : ruleDefinition.getActions()) {
mvelRule.then(action); //步骤2
}
return mvelRule;
}
public MVELAction(String expression, ParserContext parserContext) {
this.expression = expression;
compiledExpression = MVEL.compileExpression(expression, parserContext); // 使用mvel编译规则
}
步骤1和步骤2在创建规则时,easy-rules利用MVEL或SpEL表达式语言的能力,提前编译规则的条件表达式(condition)和动作表达式(action)。因此,规则的执行效率非常高。
这一点在我们准备618大促的压力测试数据中也得以体现。测试结果显示,即使峰值QPS达到1.5万,响应时间的最大值也仅为10.3ms
qps
响应时间
关于作者:
李文,转转金融技术部研发工程师