背景

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

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

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

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

架構選型

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

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

  1. DDD模式更加關注業務領域,能夠使得蘇寧金融會員系統更加聚焦會員產品的核心業務。
  2. DDD模式採用面向對象的設計,將系統模塊化,有利於實現軟體模塊的高內聚和低耦合,使得會員系統更加適合應對蘇寧業務的快速迭代。

技術實現

領域驅動設計實踐

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

圖 1 領域驅動設計拆分

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

  1. 精通業務

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

圖 2 面向過程的開發模式

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

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

圖 3 面向對象的開發模式

  1. 精通面向對象編程

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

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

圖 4 對象關係示意圖

  1. 對象創建

通過上面兩步,有了領域建模的思路,接下來需要考慮對象怎麼創建的問題了。蘇寧金融會員系統已經運行超過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的臃腫。

  1. 對象的聚合

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

如何避免低效的查詢服務

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

圖 6 CQRS設計圖

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

如何做讀寫分離設計

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

圖 7 讀寫分離設計

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

圖 8 基礎插件設計圖

如何實現插件模式設計

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

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

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

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

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

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

總結

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

  1. 重構提高了系統的性能

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

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

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

  1. 重構增加系統的可維護性

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

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

  1. 必須容易維護,將索引代碼和業務代碼解耦;
  2. 杜絕臟數據,索引數據必須和業務數據具有強一致性。

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

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

圖 9 索引更新設計

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

推薦閱讀:

相關文章