當前位置:
首頁 > 科技 > 蘇寧金融會員領域驅動設計實踐

蘇寧金融會員領域驅動設計實踐

作者 | 史輝、劉良亮

編輯 | 小智

背景介紹

近年來,蘇寧集團業務不斷擴大,用戶快速增長,線上線下融合不斷深入,系統的複雜性越來越高,技術的廣度和深度都在不斷拓展。

在整個集團技術不斷迭代演進的過程中,集團內各個系統也同步更新、迭代、重構,快速適應技術的發展,滿足業務增長的需求。

蘇寧金融會員系統作為蘇寧金融的一級系統,從易付寶誕生開始就作為基礎支撐系統為整個金融業務系統提供會員服務。經過多年的演化和業務版本的迭代維護,到如今代碼調用錯綜複雜,各個邏輯散落在代碼的各個角落,牽一髮而動全身。而且這些業務邏輯基本都集中落在了代碼的 Biz 層中,導致 Biz 層臃腫龐大。

為了適應蘇寧業務的快速發展,跟進蘇寧集團多活架構的演進,金融會員系統的技術架構需要再一次躍遷。

架構選型

重構系統的架構選型是一個仁者見仁智者見智的事情,沒有哪一種模式是標準答案,只能追求更適合的選項。本次對金融會員系統重構,從框架選型到架構選型都做了新的選擇,選擇了 Spring+Mybatis+Mycat+MySQL 的技術框架和 DDD+CQRS+ 插件的架構模式。

領域驅動設計(DDD,Domain-Driven Design)作為這一次系統重構的架構選型,主要考慮到以下因素:

DDD 模式更加關注業務領域,能夠使得蘇寧金融會員系統更加聚焦會員產品的核心業務。

DDD 模式採用面向對象的設計,將系統模塊化,有利於實現軟體模塊的高內聚和低耦合,使得會員系統更加適合應對蘇寧業務的快速迭代。

技術實現

領域驅動設計實踐

DDD 模式的最大優勢在於聚焦產品核心業務,最難搞定的也在此處。那麼該如何實現呢?領域驅動設計的關鍵在領域模型,如果把領域模型拆開來看,如下圖,就不難理解了。

圖 1 領域驅動設計拆分

那麼,理解領域驅動設計就變成如下四點內容:

1. 精通業務

精通業務,需要業務專家,對於互聯網產品,產品經理就是業務專家。技術人員作為重構發起方,需要不斷和產品經理討論業務,梳理出業務流程中隱藏的數據信息。例如會員系統的開戶服務,產品經理給出的業務流程如下:

圖 2 面向過程的開發模式

上面流程看似很清晰,按著常規思路,上面每一步對應一段代碼,按這種方式寫出來的代碼,就是大家常說的麵條代碼(或者事務腳本)。

如果採用領域驅動設計的模式來做的話,會怎麼樣?首先,和產品經理討論,註冊流程涉及哪些操作步驟,各個步驟涉及哪些數據;然後,將各個步驟的數據和對應的操作包裝起來成為一個一個對象;最後,和產品經理討論這些對象還應該具有哪些功能,各個業務功能模塊分屬於哪些對象。和產品經理的溝通不再是基於業務流程,而是基於業務模型。那麼註冊流程應該如下圖所示:

圖 3 面向對象的開發模式

2. 精通面向對象編程

在 DDD 模式中將對象分為 ValueObject 和 Entity。ValueObject 代表的是值對象,比如一個地址「南京市玄武區徐庄軟體園」,該地址沒有生命周期,可以通過對象拷貝關聯到任何一個在徐庄軟體園的個人賬戶,這就是一個 ValueObject。而 Entity 對象是有生命周期的,可以唯一標識的,該對象只能屬於某一個業務,比如 LoginPassword,一個 LoginPassword 對象只能屬於某一個 Account 所有,不能任意拷貝,並伴隨 Account 註冊而初始化,隨著 Account 註銷而刪除。

採用面向對象的編程,合理的組織對象之間的關聯、聚合、組合關係,能夠更好的遵循 SOLID 原則,能夠更好管理對象。例如易購賬號(CustNo)和易付寶賬號(UserNo)綁定關係,對於易付寶來講一個賬號要麼建立綁定關係要麼沒有建立綁定關係。如果建立綁定關係了,一個易購賬號一定對應一個易付寶賬號,那麼當我們在易付寶會員側建立 CustNo 領域對象時,和 UserNo 對象之間就是聚合的關係。當一個綁定關係建立時,該綁定關係對應的綁定關係控制器(EgoBindCtrl)也同時創建,但是一個 EgoBindCtrl 只對應一個綁定關係,如果綁定關係不存在了,那麼 EgoBindCtrl 也沒有存在的必要了,此時 CustNo 對象和 EgoBindCtrl 對象之間就是組合的關係,如下圖:

圖 4 對象關係示意圖

3. 對象創建

通過上面兩步,有了領域建模的思路,接下來需要考慮對象怎麼創建的問題了。蘇寧金融會員系統已經運行超過 8 年時間,擁有超過 3 億用戶,這麼大的數據量,如果對錶結構進行重構,是不太現實的,保持現有的數據結構,對於表結構和領域對象之間的映射關係是複雜的。我們採用 Repository 對 Domain 進行數據轉化,在 Repository 中將 DMO 轉化為 Domain,這裡有兩種模式可選擇:

圖 5 領域模型對象創建模式對比

如上方式中 Application,DomainFactory,Repository,Dao 都是採用 Spring 單例的方式管理,通過注入的方式集成,Domain 是根據業務需要 new 出來的。

如圖 A 的方式,在應用層(Application)注入 Repository 服務,在 Repository 中轉化 Domain 對象,這種方式簡單直接,但是很容易將 Repository 的服務做成事務腳本的模式,結果將業務由 Domain 轉移到 Repository 的服務中來,做成了偽 DDD 模式。如圖 B 的方式,在應用層(Application)注入 DomainFactory 服務,在 DomainFactory 中構建 Domain 對象時將 Repository 服務導入到 Domain 對象中。Application 無法直接調用 Repository 服務,只能通過 Domain 來操作 Repository 服務,這樣避免了 Repository 作為上帝之手的角色。將業務封裝在 Domain 中,最大可能的避免 Repository 的臃腫。

4. 對象的聚合

做到上面三點之後,發現這不就是面向對象編程嗎?為什麼起一個領域驅動設計這樣高大上的名字呢?沒錯,完成上面三項之後,就解決了 DDD 模式的大部分問題,還剩下的一個問題就是業務聚合。我們已經將業務封裝在模型中,但是不可能把一個領域的所有業務都封裝在一個模型中,為了完成一個領域業務會創建一系列模型,還需要考慮這些模型之間的關係,將一個模塊的業務聚合在一個聚合根下面,同一個聚合根下的所有對象只能擁有唯一的訪問入口,來保證聚合內部的一致性。例如 PaymentPassword 業務,同時還需要 PayPwdCtrl 來對支付密碼進行校驗控制,對 PayPwdCtrl 的訪問只能通過 PaymentPassword 的入口來完成。

如何避免低效的查詢服務

蘇寧金融會員系統,不僅對外提供註冊、激活、帳密安全管理等用戶生命周期的動作,同時還對多個外系統提供數據查詢服務。很多查詢服務查詢的數據會跨越多個聚合領域,如果查詢服務經過領域模型,勢必存在效率問題。因此,有必要引入另外一個設計模式讀寫分離設計(CQRS)。

圖 6 CQRS 設計圖

業內有比較成熟的 CQRS+Event Sourcing 模式,但是事件溯源(Event Sourcing)比較複雜,而且對數據存儲需要重新設計,所以在會員系統重構設計上拋棄了事件溯源模式,單獨採用 CQRS 模式。

如何做讀寫分離設計

讀寫分離本身是一個比較樸素的設計,在系統中我們常用到緩存讀寫分離,資料庫讀寫分離,那麼服務讀寫分離應該如何設計呢?在系統架構上,通常採用水平拆分來提高程序的伸縮性,採用垂直拆分來提高程序的可擴展性。垂直拆分應當是按業務來拆分,下圖 B 按讀寫分離進行垂直拆分打破了業務內聚屬性,會增加後期維護難度。

圖 7 讀寫分離設計

為了保證業務的內聚,會員服務系統採用圖 A 這種方式,所有業務落在一個系統內部。在代碼上實現讀寫分離,使用插件結構,將讀寫在設計上分離開來,對讀寫代碼分開維護,獨立演化,業務上保持一個系統的內聚。

圖 8 基礎插件設計圖

如何實現插件模式設計

插件模式就是將系統開發看成是搭積木,將一個個功能模塊做成一個個小積木。當需要一個完整功能,只需要將積木拼裝在一起就可以了,模塊在不同的功能之間可以重用。在設計上 Spring 的 IoC 恰巧給我們提供了便利性,利用 Spring 容器來管理我們的插件,當某一個介面需要某一個插件,直接注入就可以。當然,這裡還需要我們定義好標準的插口(介面)。下面給出寫服務(ManagerService)代碼示例。

1、首先需要一個插件組裝框架,這個框架通過一個抽象類 Handle 來完成,如下所示:

2、如上框架中列出了四個層級的插件,分別是 Assemble(入參組裝與校驗)、Validate(業務校驗)、Manager(業務事務)、Subsequent(事務後業務)。針對框架中的各個插件結構層級,需要一個對應的插件工廠(Factory)來組裝該層級的多個插件,如下列舉了 Manager 插件工廠代碼:

3、在上面的插件組裝框架 Handle 中還有一個對象 EmsContext,該對象構建時傳入了 RsfCmdCodeEnum。這個 RsfCmdCodeEnum 是一個至關重要的變數,這個變數由具體介面傳入的,每一個介面對應唯一的 CmdCode,下面是一個快速註冊介面的介面代碼:

4、接下來就需要對這個介面注入各層級的插件了,我們把插件組裝放在一個名為 beans-manager-facade.xml 的 XML 文件中,如下例舉了一個介面的配置:

如上 registService,bindCustService 這兩個服務都是 Spring 的 Bean,通過這種方式將多個服務插件組裝為一個大的介面級服務對外提供,不同的介面可以共用插件。

總 結

本次對蘇寧金融會員服務系統重構,採用恰當的設計模式,提高了系統的性能;完成了與異地多活的技術對接,提高系統的可靠性;增加了系統的可維護性,提高了系統的維護開發效率。

重構提高了系統的性能

例如,採用短事務,減少事務時間,提高了系統的性能。在老框架代碼中對於業務事務的管理是放在 Biz 層中介面進行統一管理,這樣帶來一個問題,如果介面中還依賴別的系統介面,會增加整個事務時間,導致一個事務長時間占著資料庫鎖無法釋放。本次重構之後的新代碼,採用插件模式,只在 Service 插件中使用事務,這樣縮小了事務範圍,減少了事務時間,顯著提高了系統的性能。

重構降低了系統的響應時間

例如使用非同步的方式管理 Subsequent(事務後業務),縮短了介面響應時間。在互聯網系統演進中,隨著業務不斷增長,系統越來越多,系統間的交互也越來越多。當一個系統處理完當前系統的數據更新之後,往往還需要處理一系列事後工作,來完成和其他系統的交互,這些交互有些需要本地計算,有些是同步交互,這些交互會增加介面響應耗時,本次重構設計了統一的事後非同步方式,對於本系統不關心的結果並且處理起來耗時的事後工作,採用非同步的方式來完成,提高了介面的響應時間。

重構增加系統的可維護性

例如重構代碼採用插件模式和邊界清晰的領域模型,增加了索引數據維護的便利性。系統按著多活改造的需求,需要放棄之前的商用資料庫,採用 Mycat+MySQL 分庫分表的方式存儲數據,原本可以通過多個不同的查詢條件查詢數據,現在只能通過分庫分表欄位來查詢數據,如果需要通過別的欄位條件查詢數據,需要對該欄位創建索引表。先通過索引表檢索分庫分表欄位,再通過分庫分表欄位檢索數據。例如蘇寧金融業務中分庫分表欄位使用會員編號,那麼用戶登錄時使用的是用戶名,此時需要通過用戶名獲取用戶信息,再對用戶名建立索引表。

索引表的維護比較麻煩,涉及業務場景多了,容易遺漏數據,系統並發高了,容易帶來臟數據。對索引數據的維護,需要達到兩個要求:

必須容易維護,將索引代碼和業務代碼解耦;

杜絕臟數據,索引數據必須和業務數據具有強一致性。

如果單看第一條,採用非同步事件就可以完成,但是加上第二條,非同步事件就無法滿足了。

在本次重構中,得益於 DTM(Data Transfer Model)的設計,採用統一上下文數據,業務維護只需要提取出需要維護的索引數據,塞到 DTM 中,具體的索引維護的事情交給框架去做。

圖 9 索引更新設計

對於蘇寧金融會員服務系統的系統重構,是我們嘗試使用領域驅動設計的第一個案例,但不會是最後一個案例,希望藉此設計模式,能夠打通產品和研發溝通的牆,使得雙方都能夠從業務領域模型中受益,使得系統能夠更加聚焦產品核心業務價值,快速適應蘇寧金融業務的發展變化。

作者介紹

史輝,蘇寧金融研發中心高級技術經理,主要負責蘇寧金融會員及互聯網研發中心的會員產品部門工作。從事軟體研發工作 12 年,具有 8 年會員領域相關開發經驗;參與金融會員系統 1.0 到 2.0 的大型項目系統重構,多次負責核心系統改造工作;擅長互聯網服務端系統應用架構,具備很強的技術領導力。

劉良亮,蘇寧金融研發中心技術經理,主要負責蘇寧金融會員及互聯網研發中心的會員系統研發工作。具有 6 年軟體研發工作經驗;參與蘇寧集團多活改造大型項目系統重構;擅長高並發服務端系統開發架構,對構建高性能服務端系統具有豐富實戰經驗。

本文彩蛋

學習架構的過程,一定是個理論結合實踐的過程。本文更多是偏向於理論的「道」,但你同時也不可缺少實踐類的「術」。InfoQ 的編輯每個月都有精心策劃一期《架構師》電子書,內容翔實、質量上佳,你可以在 InfoQ 公眾號對話框回復關鍵詞:架構,獲取 2018 年度《架構師》電子書合集下載地址。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

都說 Git 很簡單,但你真的掌握了嗎?
斯諾登:區塊鏈只是新型資料庫,比特幣終會消失

TAG:InfoQ |