引子
背景
有贊訂單導出業務隸屬於有贊交易訂單管理組,主要職能是將有贊商家的訂單數據通過報表的形式導出並提供下載給商家使用。目前承接了有贊所有的訂單導出業務,報表的字段覆蓋交易、支付、會員、優惠、發貨、退款、特定業務等,合計超過 100 個。
挑戰
隨着有贊的迅速發展,有贊的行業、業務與產品覆蓋面越來越廣。從行業角度來看,覆蓋了微商城、新零售、餐飲、美業、教育等,從模塊角度來看,覆蓋了交易、資產、客戶、營銷、店鋪等,從產品角度來看,覆蓋了分銷、精選等。每個行業、模塊、產品都會在訂單導出報表中有所訴求。如下圖所示,展示了有贊訂單導出的域模型:

訂單導出需要跨越來自不同行業、不同產品、不同模塊,對各個業務域的存儲和設計有整體理解;同時,需要通過技術手段(數據域、存儲域、報表域、文件域)聚合來自各個域的數據集合,生成可讀的報表下載給商家。
由此可見,其主要的挑戰是:如何快速支持各個域靈活多變的導出字段需求。如何應對這一挑戰呢?
架構重構
訂單導出的最初實現是從交易的多個 DB 及多個業務 API,分別獲取交易、支付、會員、發貨、退款、核銷、分銷等多個數據,組裝到一起生成報表。采用 PHP 任務腳本來實現。這種做法有兩個痛點:
- 在小量訂單導出的情形尚能應付,一旦同時有多個數萬訂單導出任務時,資源占用非常大,CPU 基本被打滿,PHP 導出進程被阻塞,從而阻塞了所有的訂單導出,導出就無法提供服務了。
- 直接訪問業務數據庫存在一種潛在風險:如果訪問業務數據庫的數據量很大,SQL 編寫不當導致慢查,往往會給業務數據庫帶來訪問壓力,嚴重影響正常核心業務流程。
基於這兩個痛點,有贊訂單管理組進行了架構升級,詳見有贊技術博文《有贊訂單管理的三生三世與“十面埋伏》。 得益於此,訂單導出也遷移到基於 ES + Hbase 的技術棧。其中訂單搜索采用 ES 服務實現,訂單詳情則存儲在 Hbase 中,通過 API 來獲取。整體流程如下所示:

重構之后,訂單導出的性能和穩定性有了很大的提升:
- 支持百萬級訂單的導出,且導出的速度比之前大幅提升。以前導出幾萬訂單慢且易阻塞,現在平均能導出 1w/1min,大多數導出可在十幾秒內完成。
- ES 和 Hbase 具有天然的彈性和容量擴展性,即使總訂單量有數量級的增長,導出的速度和穩定性也不受影響。
- 擺脫了容易被阻塞的困境,不再直接訪問業務數據庫,關閉了導出對核心業務流程的潛在威脅。
接下來,開始了配置化之旅。
配置之旅
初嘗配置-設下伏筆
訂單導出常常要面臨添加新的報表字段的需求。最初實現不太靈活,是來一個字段,在代碼流程里添加一個字段。每次增加新的字段,都需要修改多處。因此,第一個優化是采用函數接口編程,將字段定義做成枚舉可配置化的,然后遍歷指定的報表字段列表,拿到對應的字段定義,計算字段的值,寫入報表文件。
根據報表字段列表生成報表行的偽代碼如下:
public List<String> generateReportLineData(List<String> fields) {
return StreamUtil.map(fields, field -> {
try {
FieldDefinition fieldDef = getFieldDefinition(field);
FieldMethod method = getMethod(fieldDef);
String value = method.invoke(this.reportItem);
return postproc(value);
}catch (Exception e){
logger.warn("failed to get value for field: {} orderNo: {}", field, reportItem.getOrderNo());
return "";
}
});
}
這個小小的優化,為進一步的配置化設下伏筆。當需要新增報表字段時,只要增加新的字段定義,而不需要在流程里增加代碼。增強軟件可擴展性的一個重要方法是,將流程變得通用,只要增刪流程里的環節及定義即可。
凡基礎必要總是正確的方向。
報表配置-破局之時
有贊新零售、餐飲的迅速興起和發展,需要低成本快速地搭建起零售和餐飲的訂單導出。這要求訂單導出具有更大的靈活性,能夠根據不同行業的要求配置不同的字段列表及導出格式,同時又能互不影響。此外,不同商家有個性化的導出需求。然而,原來的訂單導出,是專門為微商城開發的商品級別的報表。要加一個字段,往往會影響所有的有贊商家,使用體驗不佳,訂單報表本身也變得臃腫不堪。
如何突破原來的局限,支持更靈活的訂單導出呢?這是訂單導出面臨的一個破局點。通過訂單導出模板解決了這個問題。針對行業、產品配置的導出模板存儲在 DB 表 export_biz_conf 里;針對有贊商家的導出模板存儲在 DB 表 export_customized_conf 里。每個導出模板包括了如下信息:報表字段列表、導出維度(訂單及商品)、報表文件格式、可選項等,做到足夠靈活。
若要導出不同報表字段,只要新增相應字段,指定報表字段列表即可;若要生成不同維度的報表,可使用策略模式。比如,
- 導出大訂單量,采用批量並發策略更高效;導出小訂單量,采用串行策略更易理解;
- 可以把字段定義寫在本地代碼里直接引用,或者配置在 Groovy 腳本里更加靈活;
- 可以根據指定的訂單級別或商品級別進行維度聚合,然后計算報表字段的值;
- 可以根據指定的 csv 或 excel 生成相應的文件。
如圖所示: 針對導出流程的各環節,可采用策略模式來選擇不同實現,然后將策略組合起來。

通過實現報表配置功能,突破了之前的局限,可以支持不同行業、產品的標准化和定制化導出需求,並且做到相互隔離不干擾。
配置深化-更快更穩
隨着有贊進入更多的行業,面臨着更加多變和個性化的導出需求。比如,有贊教育需要導出知識訂單的學員信息和課程信息,有贊零售需要導出導購員和發貨倉庫門店名稱。 顯然,如果要完成某個導出需求,還需要修改代碼、發布系統,這種操作會非常頻繁,導致開發和維護成本提升,影響系統穩定性。
如果能夠在應用運行中動態地新增報表字段並加載和使用,無需修改導出工程代碼,無需重新發布系統,就能更加快速地支持導出需求,將會大幅降低導出需求支持的開發和維護成本,保持系統穩定性。
為了解決這個問題,引入了動態腳本語言 Groovy. Groovy 是能夠與 Java 無縫對接的好伙伴,可以直接使用 Java 類的功能。編寫 Groovy 腳本實現報表字段邏輯,存儲在字段配置表 export_field_conf 里, 在報表配置表 export_biz_conf 或 export_customized_conf 里引用,然后在應用啟動時緩存到內存里並使用。比如粉絲姓名的 Groovy 腳本如下:
import com.youzan.trade.orderexport.util.PublicUtil
def fansInfo = reportItem.orderInfo.extra["FANS"]
PublicUtil.fetch(fansInfo, "nickname")
PublicUtil 是導出工程里封裝的一個工具類,可以讓編寫字段配置腳本更加簡單。值得提及的是,為了避免使用 Groovy 腳本可能導致的內存泄露,需要對編譯后的 Groovy 腳本進行緩存和執行。
為了實現無需改動代碼和發布系統,還需要在整體流程上打通。整體流程如下:
Step1: 當用戶下單后,源數據落到業務數據庫的擴展信息里;
Step2: 通過數據同步,自動同步到 Hbase 表;
Step3: 通過 Apollo 配置和可擴展的數據聚合機制,將數據自動輸送到用來計算報表字段值的報表對象里;
Step4: 新增報表字段的配置;
Step5: 在報表配置中引用該字段的標識。
下圖展示了通過配置自定義字段快速支持導出需求的整體流程。

整體流程打通后,當需要新增個性化字段時,通常只要做兩步:
- 增加個性化字段的配置,包括 Groovy 腳本;
- 測試通過后,刷新應用的配置即可。
個性化字段配置能力已經在線上穩定運行,比如拼團訂單成團時間、零售導購員、有贊教育的課程字段等。
通用導出-錦上添花
緊接着,訂單導出又面臨分銷采購單的導出需求。分銷采購單導出流程跟訂單導出有所不同,需要分別導出分銷買家訂單和供貨訂單的詳情信息再導出。這個流程跟通用的訂單導出流程是有所區別的。如果通過修改訂單導出的通用流程來支持,顯然會影響所有的訂單導出,使訂單導出流程不清晰。
最終采用的解決方案是:對分銷采購單的導出需求和所需技術進行抽象,實現一個更加通用的導出能力模型,支持交易領域的各種潛在導出需求,而不僅僅局限於分銷采購單導出。通過分析訂單導出流程可以發現,絕大多數導出都遵循如下核心流程:

可以將核心流程做成插件式的。首先,定義一個插件接口,包含其配置和功能等;其次,實現常用的插件列表,支持從 ES, HBase, API 查詢或獲取數據,以及常用的過濾、排序、格式化、生成報表等功能;最后,將這些插件列表串聯成一個具體的導出實例。整體流程則采用模板方法模式復用了訂單導出流程。
比如微商城分銷采購單導出通過依次執行ES查詢插件、訂單詳情插件、數據排序插件、報表字段格式化插件、報表生成插件來實現,其中訂單詳情插件針對分銷買家單和供貨訂單分別調用了一次。
質量保障
前面提到,訂單導出的報表字段非常多,導出數據量大,如何保證代碼改動或重構后訂單導出的服務質量和數據准確性?主要手段如下:

- 質量流程保障是第一位的。最主要的三項是:單測嚴格全部通過; CodeReview 由應用責任人及經驗豐富的高級工程師同時通過;預發線上導出對比工具通過。
- 整體架構設計保證了訂單導出的性能、穩定性和可擴展性。
- 持續小幅重構使得系統能夠持續優化,避免一次性大改造傷筋動骨且容易導致線上故障。
- 設計先行,對代碼質量非常重視。
- 運行預發線上訂單導出自動化對比工具,很大程度上增強了成功發布的信心,是發布前保障質量的一道重要防線。
此外,采用函數編程及設計模式,使代碼實現層面更具復用性和柔軟性。18K 行代碼,代碼重復率約為 1.8%。
小結與致謝
本文簡要講述了有贊訂單導出的配置化實踐。通過配置化之后,訂單導出的能力和穩定性有了大幅提升。當然,還有一些需要提升的地方。比如,可以增加擴展點機制,允許業務方定制化導出;局部細節可以打磨得更細膩。歡迎對海量訂單業務感興趣有經驗的小伙伴與我們一起共建訂單管理大局!簡歷可直郵 shuqin@youzan.com.
在這個過程中,有許多小伙伴給予了有力的支持,比如產品同學對訂單報表的細致的規划設計、客滿運營同學提出的及時反饋、有贊技術團隊的支持以及自己的付出。
更多技術文章,詳見有贊公眾號: 有贊coder 及 有贊技術博客

