基於Event Sourcing和DSL的積分規則引擎設計實現案例

架構設計模式(Architecture Patterns),是“從特殊到普遍”的、基於各種實際問題的解決方案而總結歸納出來的架構設計最佳實踐,是一種對典型的、局部的架構邏輯的高度抽象思維;在合理的場景下恰當使用它們,避免“重新發明車輪”,對技術解決方案有指導性作用,往往事半功倍。廣發證券IT研發團隊作爲架構設計模式的堅定踐行者,在各類證券業務中經常運用。Event Sourcing就是這麼一個比較常用而重要的架構模式。本文介紹的雖然是金融業場景,但是“積分系統”相信對其他行業的開發者也不會陌生。技術團隊嘗試用Event Sourcing架構模式和基於Go構建的DSL“簡單而優雅”的解決一個問題。

在電商行業,積分幾乎已經成爲了一個標配。 京東、淘寶都有自己的積分體系。 用戶通過購物或者完成指定任務來獲得積分。累積的積分可以給用戶帶來利益,比如增加用戶等級,換取禮品或者在購物時抵扣現金。

在廣發證券的金融電商運營平臺中,積分同樣是一個不可或缺的基礎服務,很多應用都有和積分賬戶交互的場景。積分的用途也比較廣泛,除了用在面向客戶的服務中增加客戶粘性和忠誠度外,積分也被用來支持內部的“遊戲化”(gamification)運營,讓數字化經營成爲可能。 例如:公司的投資顧問可以通過編輯高質量的理財知識條目和回答客戶問題獲得積分,最終被換算回個人績效收入。


一個場景,是客戶提了一個關於證券的問題, 如果投資顧問回答了這一問題,並且答案被其他用戶收藏,就可以獲得500積分作爲獎勵。 實踐表明,積分的使用大大提升了投資顧問回答問題的積極性,提高了運營的效率。這其實是精細化運營、數字化經營的一個非常重要的基礎設施。這個積分體系的存在,甚至改變、顛覆了傳統企業對員工進行分派任務、管理、激勵、計算個人績效的機制。

從技術的角度,怎樣實現一個積分系統滿足各種應用程序的需求呢? 雖然使用積分的場景不同, 有的面向客戶,有的面向公司內部的理財顧問,進行抽象後的積分系統可以是相同的。 和銀行賬戶類似, 一個用戶的積分賬戶可以看作由賬戶類型, 表示餘額的數字和一系列引起積分變化的流水帳組成。 根據這些共性,我們把積分實現爲一個獨立的服務,統一存儲管理積分數據。 在和應用程序交互的方式上,最初的想法是積分系統爲應用程序提供增加/扣除積分的接口, 由應用程序決定增加/扣除積分的數量。

我們很快發現這種架構在應用程序中嵌入了積分規則邏輯,當積分規則改變時,應用程序需要隨之改變。 比如,如果運營人員把上面例子中的500積分改變爲1000分後,開發人員就需要升級應用程序。 在使用積分的應用程序數量多,運營需求變化快的情況下,這種應用程序和積分系統緊密耦合的架構增加了系統維護成本。

典型的“事件驅動”場景

無論是面向消費者客戶的電商平臺、航空公司顧客的飛行裏數服務(mileage program)還是面向內部員工的“遊戲化運營”平臺,很顯然,在技術層面都是一個典型的“事件驅動”場景 – 用戶通常通過在各種各樣的業務系統進行了一些活動,這些活動被記錄到一個積分系統中“映射”成一定的積分。所以,積分系統設施的“使用者”,往往是其他的一些各式其色的、事前甚至無法預估的應用程序。

技術架構的設計原則是這樣:由不可預知的應用程序自己負責判斷其用戶所進行的活動有無“價值”,對於有價值的活動則以發起事件的方式異步通知積分系統,積分系統則負責實時收集事件並基於各種可能由經營管理者隨時修訂、配置、改變的積分規則對事件所包含的用戶活動進行“簿記”(book-keeping)。

我們採用了基於消息總線的架構設計, 應用程序和積分系統之間通過異步的消息總線關聯。應用程序不包含任何積分規則,只負責向消息總線發佈事件。 積分系統被實現爲一個獨立的服務,包含了所有的積分賬戶數據和積分規則。 積分系統向消息總線訂閱事件, 然後根據設置的積分規則處理事件, 記錄積分。這種架構使應用程序和積分系統呈鬆耦合關係,提升了系統的可維護性。 系統架構如下圖所示:

舉一個例子說明記錄積分的過程:某投資顧問在廣發證券知識庫的應用程序中回答了一個問題,並且該問題被一個客戶收藏。 知識庫應用程序向消息總線發佈一個答案被收藏的事件。 積分系統在監聽到這一事件後,根據事先配置的積分規則,向投資顧問的積分賬戶增加積分數量,記錄積分流水。

積分系統監聽的事件並不一定由應用程序直接產生。對於複雜的積分規則,可能由其他服務處理應用程序的事件流後,產生新的事件流,再由積分系統處理。例如,需要對7月份連續3天登錄的用戶獎勵50分。應用程序沒有保存歷史登錄數據,只產生簡單的登錄事件。 大數據平臺(對於積分系統而言是一個應用程序)可以根據保存的歷史數據產生包含連續登錄天數的事件, 發佈到消息總線上後由積分系統訂閱處理。 這體現了基於消息總線架構的優點,能把積分處理邏輯從應用程序中完全剝離出來,同時具有擴展性。

積分類似虛擬貨幣,可最終換算成員工績效或者消費者的某些形式的獎勵,所以不能多記,也不能少記。爲了達到這一目標,技術層面上需要解決消息被處理一次且僅被處理一次的問題。我們的消息總線採用的是分佈式消息系統Kafka, 它具有比較好的容錯性和擴展性, 但不直接提供這樣的支持,需要在應用程序層面處理。 應用程序向kafka發送消息時可能因爲網絡的原因發送失敗。

爲了避免丟失用戶積分,我們要求應用程序在向Kafka發送消息失敗後進行重試。但這樣又有可能出現同一個積分事件被重複接收導致多記積分的問題。 我們的解決辦法是應用程序在產生積分的事件中帶上一個對用戶唯一的uuid, 並且通過重發的機制確保事件最少被髮送到Kafka一次。 在積分系統中根據uuid進行排重,丟掉uuid重複的積分事件,保證積分事件最多被處理一次。通過這樣一種應用程序之間的協議實現了一個積分事件被被處理一次且僅被處理一次的目標。

Event Sourcing 架構模式

在實踐中,我們有修正積分的需求。 比如, 由於bug, 應用程序錯誤的產生出了一些事件,需要減掉由這些事件而增加的積分。直接的方法是找出這些事件產生的積分,然後從賬戶中直接扣減。 但是這一方法在下面的場景中會導致錯誤:

假設積分規則是用戶首次登錄獎勵500分,當天內第2次登陸再獎勵1000分。

  1. 由於應用程序錯誤,產生了登錄事件L1,導致增加500積分

  2. 用戶登錄產生登陸事件L2。 積分系統發現當天已經出現過1次登陸事件L1, 根據規則增加了1000積分。

管理員發現爲L1不應該發生,直接扣除500積分,用戶實際得分1000分。 這是錯誤的。 在沒有事件L1的情況下,登陸事件L2只應該獲得500分。產生這一錯誤的根本原因是積分的計算可能依賴於歷史事件。 歷史事件的變化將影響後續事件處理。

解決這種問題的一種方式是:當歷史事件發生了變化時, 回滾到該時間點前的歷史狀態,然後按照時間順序重新處理之後的所有積分事件, 這類似於數據庫系統中使用checkpoint和日誌來恢復數據庫狀態的方式。Event Sourcing 概括了這種軟件設計模式(詳細內容可參考軟件設計領域大師Martin Fowler的相關文章)。Event Sourcing 模式最核心的概念是程序的所有狀態改動都是由事件觸發並且這些事件被持久化到磁盤中。 當需要恢復程序狀態時,只需把保存的事件讀出來再重新處理一遍。

積分系統遵照Event Sourcing模式實現。 積分的所有變化都由積分事件觸發,所有積分事件都存儲在數據庫中。爲了回滾積分賬戶狀態,還需要保存積分賬戶的歷史數據。我們實現的方法是在積分賬戶發生變化時,產生一條積分流水,保存了積分變化數量,以及積分變化前和變化後的總額。當需要回滾積分賬戶狀態時,找到離回滾時間點最近的積分流水,恢復歷史積分賬戶的總額,然後按照時間順序逐一處理保存的積分事件,恢復積分賬戶數據。 下圖展示了這一流程:

下面是用命令行工具把積分賬戶狀態恢復到2016-05-01之前,然後重新處理積分事件恢復積分的界面。

在生產環境的運維經驗表明,相對於手工直接修改積分賬戶數據, 這種修改歷史積分事件,回滾賬戶狀態然後重新處理積分事件的方式不但提高了準確性,而且簡化了修正工作,節省了運維人員的時間。

用Go構建DSL實現靈活的積分規則引擎

由於接入的應用程序類型多樣,積分規則會隨着運營的開展而頻繁變化。如果每次積分規則發生了變化,都要求對積分系統改動升級, 積分系統維護就會變成一項很繁瑣的工作。 我們的目標是讓積分系統保持足夠的靈活性,當積分業務規則變化時,在大多數情況下可以不用改動升級積分系統。最理想的情況是運營人員通過簡單培訓後自己就能配置積分規則,不需要開發人員修改積分系統軟件。

爲此我們開發了一個積分規則引擎, 通過提供一個積分規則描述語言,把積分的業務邏輯從積分系統軟件中分離出去:

下面首先描述積分規則描述語言的語法表示和存儲方式, 然後描述規則引擎加載解釋積分規則的流程。

積分規則引擎首先需要提供一個讓運營人員描述積分規則的語法。抽象的看,積分規則可以表示爲一個元組: (積分條件,積分數量), 表示當滿足設置的條件時,增加對應的積分數量。 很容易聯想到積分條件可以用編程語言中的布爾表達式表示,積分數量用數值表達式表示。

由於我們使用的是Go語言實現積分系統, 出於解析方便的考慮(Go自帶了自身的語法分析庫),我們採用了Go語言的表達式語法表示積分規則的條件和數量。 在積分規則的表達式中,Go語言的字符串、數字、布爾常量都可以直接使用。變量表示積分事件中的字段數據。比如,積分規則(event_type==“answer_is_liked”, 250) 表示當前積分事件類型(event_type)爲answer_is_liked(答案被點贊) 時,積分條件匹配, 記錄 250 個積分 。

在定義了積分規則的語法表示後,還需要決定在哪裏存儲積分規則。最初考慮存放在文件中,很快發現如果把積分規則和積分數據存放在同一個數據庫中就可以方便的利用數據庫的一致性檢查功能保證數據一致性, 這是保證軟件系統長期正確運行的關鍵措施。 比如,通過數據庫的外鍵設置,我們能保證每條積分流水指向一個有效積分規則,杜絕因爲規則被錯刪,積分流水指向無效積分規則的情況。 下面是積分規則在數據庫中表示的例子:

上表第1行積分規則表示當積分事件是answer_is_liked時,增加250分;

第2行要複雜一些,表示當積分事件是answer_question(回答問題),並且屬於首次回答問題時增加積分, 如果是投資顧問,增加4000分,其他人員增加2500分。其中event_type是積分事件的字段; count_by_same_event_attr是在規則表達式中允許使用的函數,用來統計該用戶的具有相同字段值的積分事件數量;data.originator_type 也是積分事件的字段,表示用戶類型。

爲了增強擴展性,規則引擎提供了一套插件機制,可以用Go語言編寫能用在規則表達式中使用的函數。比如上表第2行中的count_by_same_event_attr就是通過插件實現的,用來計算目前已經收到的具有相同屬性值的事件數量。在實踐中,當發現積分規則不能滿足業務需求時,我們往往通過編寫插件的方式來擴展積分規則的表達能力,而不是修改規則引擎的核心代碼。

在運營人員配置積分規則後,積分系統需要使用規則引擎解釋執行積分規則, 主要流程是:

1、積分系統在啓動時加載所有應用程序的積分規則

積分規則在被規則引擎加載後完成語法解析,在內存中解釋執行。 這避免了在運行中訪問磁盤或數據庫引起的性能瓶頸。需要注意的是,雖然積分規則的語法和Go語言表達式相同,積分規則的語義卻有變化。對於會引起Go語言拋出異常的表達式(e.g. 除 0),積分規則引擎解釋爲nil,避免了程序異常退出。

2、監聽消息總線,對於新收到的積分事件,逐個嘗試匹配積分規則的條件。如果該積分事件能滿足某個積分規則的條件,則增加由積分規則中的積分。

下圖表示了運行規則引擎記錄積分的流程。

可以看出,我們實際上構造了一個DSL(Domain Specific Language), 語法和Go語言的表達式一樣,但是語義不同。積分規則其實是這一DSL編寫的程序, 作爲數據保存在數據庫中, 在被規則引擎裝載後又當作程序來執行。這裏體現了“代碼即數據”(code as data)的編程思想。

技術棧:Go + Postgres + Docker

1、Go 語言

Go語言是爲大規模系統軟件的開發而設計的, 具有語法簡潔,靜態類型檢查,編譯快速,支持併發程序設計等特點。

和JavaScript等動態語言相比,我們感覺在某些場景下,由於Go的類型系統比較複雜並且不支持範型, 編寫的代碼量會多一些。一個典型的例子是排序,使用Go的排序庫時,一般需要實現一個sort.Interface, 包含有Len, Swap, Less 3個方法。 而使用JavaScript進行排序,往往只需要1行代碼。

但是和動態語言相比,Go的靜態類型檢查減少了很多運行時bug,節約了調試時間, 並且Go提供的工具比較完善,自帶文檔,格式化,單元測試和包管理工具。 Go的生態系統也比較成熟,第3方軟件包豐富。綜合來看,使用Go的開發效率並不會低太多。

我們發現Go語言的靜態鏈接特性非常適合docker部署,積分系統用docker打包後只有10M左右。相比於NodeJS打包後上百M的體積,採用Go語言大大節省了部署時間和資源。

總的來說,我們對Go語言是比較滿意的,將會繼續在關鍵的系統服務中使用。

2、Postgres

在使用了一段時間的MongoDB後,我們希望在關鍵業務中採用有嚴格schema檢查的關係型數據庫。 Postgres是一個成熟的開源數據庫,除了支持數據一致性檢查和事務外,也支持JSON, 吸收了NoSQL的優點。

在積分系統中,應用程序需要在積分事件中保存一些自定義的屬性, 在查詢積分流水時積分系統原樣返回,由應用程序自行處理。 由於事先無法預知應用程序保存的內容格式,我們把這樣的數據放在一個JSON字段中, 完全由應用程序控制。在數據存入之後, 通過Postgres的JSON操作符,我們可以方便的管理這些數據,比如,根據指定的JSON字段查詢。

除了使用Go、Postgres、Docker這些技術開發和部署服務,由於積分系統是爲應用程序提供服務的,它天然需要通過API來支持其他開發者。 我們選擇了用工具slate來製作API文檔。下圖是使用markdown編寫,由slate轉換成html格式的 API文檔式樣。

總結

積分系統並不是一個技術架構上覆雜的系統,但是它是借鑑“遊戲”實踐而進行的數字化精細化經營的重要業務環節,相信在越來越多進行“互聯網+”創新的垂直行業中會有類似的實踐。具體的技術實現手段也很多,在此爲便於行業內外讀者的理解,我們對方案作了簡化和抽象。

然而,對相對簡單的問題作“教科書”式的簡練實現,遵循KISS(Keep It Simple,Stupid!)的原則,避免“過度工程”(over-engineering),也是我們的團隊文化和準則。本文所介紹的Event Sourcing架構模式和DSL規則引擎,可以幫助我們在很多場景“簡單而優雅”(simple but elegant)的解決問題。

發佈了15 篇原創文章 · 獲贊 115 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章