解鎖轉轉門店業務靈活性:如何利用MVEL引擎優化結算流程
1 業務現狀
隨著門店結算業務的不斷擴展,我們面臨了日益增長的復雜性。目前,需要聚合計算的結算指標數量龐大,每個指標都依托于一套復雜的公式,而這些公式又是由眾多業務配置參數構成的。業務的復雜化導致需要維護的公式數量急劇增加,帶來了一系列挑戰:
- 配置分散問題:業務配置目前分散在代碼、
Apollo
配置中心以及數據庫中,這種分散性使得維護工作變得繁瑣且低效。 - 頻繁更新問題:隨著業務的不斷調整,結算公式需要頻繁更新。每一次微小的改動都要求進行系統上線,這增加了開發的負擔。
- 代碼維護問題:每次新公式的上線,都需要保留舊版本的指標公式。這導致在代碼中需要同時維護多套指標公式,嚴重影響了代碼的可讀性和可維護性。
指標計算流程
基于這些問題,我們的優化方案是建立一個公式管理中心,將所有的這些指標運算進行收攏。同時引入了強大的表達式引擎來處理這些運算,本文就如何使用表達式引擎解決這些問題展開分析。
2 調研分析
2.1 為什么選擇表達式引擎
在門店結算業務的核心環節,我們專注于對關鍵指標的公式進行精確計算,并有效管理不同版本的公式。通過引入表達式引擎,我們能夠將計算邏輯從業務代碼中解耦,實現業務邏輯與計算邏輯的分離。這種方法不僅集中化了指標公式的管理,而且由于許多表達式引擎原生支持高精度的BigDecimal
類型,它還確保了金融級精度的貨幣計算需求得到滿足。
此外,表達式引擎的動態執行特性允許我們在不重新部署的情況下實時更新公式,這樣的靈活性對于快速響應業務需求變化至關重要,大大提升了業務調整的敏捷性和系統的可維護性。
2.2 表達式引擎的對比
本文主要對幾種常見的表達式引擎AviatorScript
MVEL
QLExpress
OGNL
進行對比分析。
2.2.1 簡介
AviatorScript
: 是一款高性能、輕量級的Java語言實現的表達式求值引擎,Aviator可直接將表達式編譯成Java字節碼,交給JVM去執行。MVEL(MVFLEX Expression Language)
: 是一種動態/靜態的可嵌入的表達式語言和為Java平臺提供Runtime(運行時)的語言,在很大程度上受到了Java語法的啟發,支持解釋模式執行,也支持編譯模式執行。QLExpress(Quick Language Express)
: 是阿里巴巴開源的一門動態腳本引擎解析工具,起源于阿里巴巴的電商業務,旨在解決業務規則、表達式、數學計算等動態腳本的解析問題。具有線程安全、高效執行、代碼依賴小等特性。OGNL(Object-Graph Navigation Language)
: 即對象圖導航語言,是一種功能強大的開源表達式語言,通過簡單一致的表達式語法,可以存取對象的任意屬性,調用對象的方法,遍歷整個對象的結構圖,并實現字段類型的轉化等功能,常用于Java中。
2.2.2 性能分析
性能測試工具使用JMH(Java Microbenchmark Harness)
,是由 OpenJDK/Oracle 官方發布的工具,他們對JIT和JVM對于基準測試影響非常了解,能得到一個更好的結果。
在當前的業務場景中,主要對帶有變量和條件判斷的表達式進行高精度的求值,測試表達式:(cate==101&&brand==1276)?((a*18 +b*3)*x/y)-c%3+99.64 : a*18
在本機環境下,執行五次,AviatorScript
的性能要略優于 OGNL
優于 MVEL
,前三者的性能遠遠優于QLExpress
。
2.2.3 社區活躍度
社區活躍度主要看這幾個項目在GitHub
上的 Star
、Fork
、Watch
、Last Commit
來進行分析,截止到發稿時間的對比如下:
項目 | Star | Fork | watch | Last Commit |
AviatorScript | 4.4K | 821 | 171 | Jun 11, 2024 |
MVEL | 1.1K | 305 | 78 | May 16, 2024 |
OGNL | 215 | 77 | 19 | Jul 21, 2024 |
QLExpress | 4.7K | 1.1K | 215 | Jul 16, 2024 |
通過對比可以看到 AviatorScript
、MVEL
、QLExpress
的 Star、Fork、Watch 更高,說明他們的影響力更高,更受歡迎。
2.3 最終選擇
通過以上的對比分析,最終選擇使用 MVEL
,因為在性能、社區活躍度上都有很大的優勢,在語法上更加的接近Java語法,更容易上手。在一些開源項目中都有使用如:Drools
、Quartz Scheduler
、JBPM
等。MVEL
的執行流程:
MVEL執行流程
每次執行都要去解析,編譯,再執行表達式,這種在表達式執行比較頻繁的場景下會很消耗性能。MVEL
提供了兩種執行模式來應對不同的需求:
解釋模式:這種模式在每次執行時都會重新編譯表達式,雖然提供了動態執行的能力,但頻繁的編譯過程會顯著影響性能。
編譯模式:編譯模式通過將表達式預先編譯成字節碼,然后在后續執行中直接運行這些字節碼,從而避免了每次執行時的編譯開銷。這種方法顯著提高了執行效率,但需要一種機制來處理在系統運行期間對公式的實時更新。
通過這兩種模式,我們可以根據實際需求選擇最合適的執行策略,以實現性能和靈活性的最佳平衡。
3 業務應用
3.1 整體設計
抽取出三個模塊,配置中心、公式管理中心、公式運算中心。配置中心維護指標配置數據,公式中心維護指標公式,公式運算中心在前兩者維護好的基礎上運算獲取結果。
新的指標運算流程
3.2 表結構設計
需要兩張表來存儲配置數據,業務指標配置和計算公式配置。
CREATE TABLE `business_config` (
`id` bigint(20) NOT NULL DEFAULT '0' COMMENT '主鍵',
`indicator_key` varchar(255) NOT NULL DEFAULT '' COMMENT '指標key',
`attribute_key` varchar(255) NOT NULL DEFAULT '' COMMENT '屬性key',
`attribute_desc` varchar(255) NOT NULL DEFAULT '' COMMENT '屬性詳細描述',
`attribute_value` varchar(255) NOT NULL DEFAULT '' COMMENT '屬性值',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`create_by` varchar(32) NOT NULL DEFAULT '' COMMENT '創建人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新時間',
`update_by` varchar(32) NOT NULL DEFAULT '' COMMENT '更新人',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_indicator` (`indicator_key`) USING BTREE COMMENT '指標唯一索引'
) ENGINE=InnoDB COMMENT='業務指標配置表';
CREATE TABLE `formula_config` (
`id` bigint NOT NULL DEFAULT 0 COMMENT '主鍵',
`indicator_key` varchar(255) NOT NULL DEFAULT '' COMMENT '指標key',
`formula` varchar(500) NOT NULL DEFAULT '' COMMENT '公式',
`effective_timestamp` bigint NOT NULL DEFAULT 0 COMMENT '生效的時間戳',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`create_by` varchar(32) NOT NULL DEFAULT '' COMMENT '創建人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新時間',
`update_by` varchar(32) NOT NULL DEFAULT '' COMMENT '更新人',
PRIMARY KEY (`id`)
INDEX `idx_business_key`(`business_key`) USING BTREE
) ENGINE=InnoDB COMMENT='計算公式表';
3.3 公式運算主代碼流程
public BigDecimal cal(String businessKey, Map<String, Object> paramMap) {
//1.根據businessKey 獲取配置數據
Map<String, Object> configMap = qfConfigService.getConfigMapByBusinessKey(businessKey);
//添加業務單據參數
configMap.putAll(paramMap);
//2.根據businessKey 和時間戳獲取 計算公式
String formula = getFormula(businessKey, System.currentTimeMillis());
//3.引擎計算
return MvelExecutor.evalExpression(formula, configMap);
}
所有的指標運算都復用了同一套運算邏輯,配置和公式解耦。
這里說的兩者之間的解耦并不是公式一點都不關心運算需要的配置參數,而是指兩者在遵守約定的前提下,在公式運算中,會根據屬性配置自動填充公式的參數。
舉個例子,現在有一個指標的公式為 (cate==101&&brand==1276)?26:38
在這個公式中有 cate 和 brand 兩個參數,這兩個參數會提前在配置中心配好,在配置表中就是 attribute_key
這個字段。attribute_key 和 attribute_value 會作為表達式運算參數的 key 和 value 參與運算。
Object object = MVEL.executeExpression(expression, paramMap);
3.4 編譯模式下的緩存策略
考慮到系統的性能問題,項目中使用了編譯執行模式,通過一次性編譯并緩存結果,實現了多次高效運行。這就需要在系統運行過程中,對于實時改變的公式,能夠及時刷新緩存,公式及時生效。由于隨著業務的發展,指標越來越多,使用本地緩存,可能會造成內存占用過高,所以使用Redis
緩存編譯后的公式。每次公式修改,就刪除緩存,下次執行重新編譯,從而確保緩存中始終存儲的是最新版本的公式。
/**
* 執行表達式
**/
public BigDecimal evalExpression(String expression, Map<String, Object> map) {
Serializable cache = getCache(DesEncryptUtil.encrypt(expression));
Object object = MVEL.executeExpression(cache, map);
return (BigDecimal) object;
}
private Serializable getCache(String expression) {
String cacheExpression = redisUtils.get(expression);
if (StringUtils.isNotEmpty(cacheExpression)) {
return JsonUtil.silentString2Object(cacheExpression, Serializable.class);
}
Serializable compileExpression = MVEL.compileExpression(expression);
redisUtils.setex(expression,ONE_DAY,JsonUtil.silentObject2String(compileExpression));
return compileExpression;
}
3.5 業務指標遷移
明確了設計方案后,具體的遷移過程不是一蹴而就的,要考慮在不影響線上業務的前提下,有計劃的逐步完成。遷移主要分代碼邏輯遷移和配置遷移,新的遷移邏輯已經在上文的設計方案里有介紹了,不同的指標運算是一個統一的調用入口,只需要在不同的指標運算處替換即可。配置遷移主要包含指標配置遷移和公式遷移。
具體遷移過程分五步進行:
- 代碼邏輯遷移
將指標運算邏輯替換為新邏輯。 - 指標配置整理入庫管理
整理代碼中、Apollo配置中、數據庫中不同的指標配置,包括歷史改變的版本,都加入配置表,以生效時間判定生效的版本。 - 公式整理入庫管理
遷移前所有的公式都在代碼中,把代碼中的計算公式,同樣包含歷史的版本,轉化為MVEL
表達式,加入公式表,以生效時間判定生效的版本。 - 數據準確性驗證
以線上最近兩個月的數據為數據源,計算遷移后指標的運算結果,與遷移前的指標運算結果作對比。如果有不一致的結果,定位原因并修復,然后重新跑數據對比,直到完全一致為止。 - 灰度&全量
先在2個門店開放新邏輯,先灰度幾個指標,如果沒有問題,就開放所有指標,最后再開放全量門店。
4 總結
本文就如何在業務中使用MVEL
表達式引擎進行了分析,旨在解決當前結算系統面臨的若干關鍵問題:
- 集中管理配置:通過建立一個公式管理中心,實現配置的統一管理,簡化維護流程。
- 即時生效的公式修改:對公式很小的改動,直接修改公式立即生效,無需代碼上線。
- 降低代碼復雜度:通過將公式的歷史版本存儲在數據庫中,并根據時間戳獲取當前生效的公式,減少了代碼中多套公式的維護負擔。
當然實際使用,還需結合具體的使用場景具體分析決定是否要使用,對于比較簡單的場景,沒有必要引入,這樣會增加系統的復雜度,一定是系統存在痛點情況下的綜合考量。