規則引擎技術在轉轉錢包的實踐
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
響應時間
關于作者:
李文,轉轉金融技術部研發工程師