1. 引言
從接觸領(lǐng)域驅(qū)動設(shè)計的初學(xué)階段,到實現(xiàn)一個舊系統(tǒng)改造到DDD模型,再到按DDD規(guī)范落地的3個的項目。對于領(lǐng)域驅(qū)動模型設(shè)計研發(fā),從開始的各種疑惑到吸收各種先進(jìn)的理念,目前在技術(shù)實施這一塊已經(jīng)基本比較成熟。在既往經(jīng)驗中總結(jié)了一些在開發(fā)中遇到的技術(shù)問題和解決方案進(jìn)行分享。
因為DDD的建模理論及方法論有比較成熟的教程,如《領(lǐng)域驅(qū)動設(shè)計》,這里我對DDD的理論部分只做簡要回顧,如果需要了解DDD建模和基礎(chǔ)的理論知識,請移步相關(guān)書籍進(jìn)行學(xué)習(xí)。本文主要針對我們團(tuán)隊在DDD落地實踐中的一些技術(shù)點進(jìn)行分享。
2. 理論回顧
理論部分只做部分提要,關(guān)于DDD建模及基礎(chǔ)知識相關(guān),可參考 Eric Evans 的《領(lǐng)域驅(qū)動設(shè)計》一書及其它理論書籍,這里只做部分內(nèi)容摘抄。
2.1.1 名詞
領(lǐng)域及劃分:領(lǐng)域、子域、核心域、通用域、支撐域,限界上下文;
模型:聚合、聚合根、實體、值對象;
實體
是指描述了領(lǐng)域中唯一的且可持續(xù)變化的抽象模型,有ID標(biāo)識,有生命周期,有狀態(tài)(用值對象來描述狀態(tài)),實體通過ID進(jìn)行區(qū)分;
每個實體對象都有唯一的 ID。我們可以對一個實體對象進(jìn)行多次修改,修改后的數(shù)據(jù)和原來的數(shù)據(jù)可能會大不相同。比如商品是商品上下文的一個實體,通過唯一的商品 ID 來標(biāo)識,不管這個商品的數(shù)據(jù)如何變化,商品的 ID 一直保持不變,它始終是同一個商品。
在 DDD 里,這些實體類通常采用充血模型,與這個實體相關(guān)的所有業(yè)務(wù)邏輯都在實體類的方法中實現(xiàn)。
聚合根
聚合根是實體,是一個根實體,聚合根的ID全局唯一標(biāo)識,聚合根下面的實體的ID在聚合根內(nèi)唯一即可;
聚合根是聚合還原和保存的唯一入口,聚合的還原應(yīng)該保證完整性即整存整??;
聚合設(shè)計的原則
聚合是用來封裝真正的不變性,而不是簡單的將對象組合在一起;
聚合應(yīng)盡量設(shè)計的小,主要因為業(yè)務(wù)決定聚合,業(yè)務(wù)改變聚合,盡可能小的拆分,可以避免重構(gòu),重新拆分
聚合之間的關(guān)聯(lián)通過ID,而不是對象引用;
聚合內(nèi)強(qiáng)一致性,聚合之間最終一致性;
值對象
值對象的核心本質(zhì)是值,與是否有復(fù)雜類型無關(guān),值對象沒有生命周期,通過兩個值對象的值是否相同區(qū)分是否是同一個值對象;
值對象應(yīng)該設(shè)計為只讀模式, 如果任一屬性發(fā)生變化,應(yīng)該重新構(gòu)建一個新的值對象而不是改變原來值對象的屬性;
領(lǐng)域事件
在事件風(fēng)暴過程中,會識別出命令、業(yè)務(wù)操作、實體等,此外還有事件。比如當(dāng)業(yè)務(wù)人員的描述中出現(xiàn)類似“當(dāng)完成…后,則…”,“當(dāng)發(fā)生…時,則…”等模式時,往往可將其用領(lǐng)域事件來實現(xiàn)。領(lǐng)域事件表示在領(lǐng)域中發(fā)生的事件,它會導(dǎo)致進(jìn)一步的業(yè)務(wù)操作。如電商中,支付完成后觸發(fā)的事件,會導(dǎo)致生成訂單、扣減庫存等操作。
在一次事務(wù)中,最多只能更改一個聚合的狀態(tài)。如何一個業(yè)務(wù)操作涉及多個聚合狀態(tài)的更改,可以采用領(lǐng)域事件的方式,實現(xiàn)聚合之間的解耦;在聚合根和跨上下文之間實現(xiàn)最終一致性。聚合內(nèi)數(shù)據(jù)強(qiáng)一致性,聚合之間數(shù)據(jù)最終一致性。
事件的生成和發(fā)布:構(gòu)建的事件應(yīng)包含事件ID、時間戳、事件類型、事件源等基本屬性,以便事件可以無歧義地在不同上下文間傳播;此外事件還應(yīng)包含具體的業(yè)務(wù)數(shù)據(jù)。
領(lǐng)域事件為已發(fā)生的事務(wù),具有只讀,不可變更性。一般接收消息為異步監(jiān)聽,處理的后續(xù)處理需要考慮時序和重復(fù)發(fā)送的問題。
2.1.2?聚合根、實體、值對象的區(qū)別?
從標(biāo)識的角度:
聚合根具有全局的唯一標(biāo)識,而實體只有在聚合內(nèi)部有唯一的本地標(biāo)識,值對象沒有唯一標(biāo)識;
從是否只讀的角度:
聚合根除了唯一標(biāo)識外,其他所有狀態(tài)信息都理論上可變;實體是可變的;值對象是只讀的;
從生命周期的角度:
聚合根有獨立的生命周期,實體的生命周期從屬于其所屬的聚合,實體完全由其所屬的聚合根負(fù)責(zé)管理維護(hù);值對象無生命周期可言,因為只是一個值;
2.2 建模方法
2.2.1 事件風(fēng)暴
事件?暴法類似頭腦?暴,簡單來說就是誰在何時基于什么做了什么,產(chǎn)?了什么,影響了什么事情。
在事件風(fēng)暴的過程中,領(lǐng)域?qū)<視驮O(shè)計、開發(fā)人員一起建立領(lǐng)域模型,在領(lǐng)域建模的過程中會形成通用的業(yè)務(wù)術(shù)語和用戶故事。事件風(fēng)暴也是一個項目團(tuán)隊統(tǒng)一語言的過程。
2.2.2 用戶故事
用戶故事在軟件開發(fā)過程中被作為描述需求的一種表達(dá)形式,并著重描述角色(誰要用這個功能)、功能(需要完成什么樣子的功能)和價值(為什么需要這個功能,這個功能帶來什么樣的價值)。
例:
作為一個“網(wǎng)站管理員”,我想要“統(tǒng)計每天有多少人訪問了我的網(wǎng)站”,以便于“我的贊助商了解我的網(wǎng)站會給他們帶來什么收益。
通過用戶故事分析會形成一個個的領(lǐng)域?qū)ο?,這些領(lǐng)域?qū)ο髮?yīng)領(lǐng)域模型的業(yè)務(wù)對象,每一個業(yè)務(wù)對象和領(lǐng)域?qū)ο蠖加型ㄓ玫拿~術(shù)語,并且一一映射。
2.2.3 統(tǒng)一語言
在事件風(fēng)暴和用戶故事梳理過程及日常討論中,會有越來越多的名詞冒出來,這個時候,需要團(tuán)隊成員統(tǒng)一意見,形成名詞字典。在后續(xù)的討論和描述中,使用統(tǒng)一的名稱名詞來指代模型中的對象、屬性、狀態(tài)、事件、用例等信息。
可以用Excel或者在線文檔等方式記錄存儲,標(biāo)注名稱,描述和提取時間和參與人等信息。
代碼模型設(shè)計的時侯就要建立領(lǐng)域?qū)ο蠛痛a對象的一一映射,從而保證業(yè)務(wù)模型和代碼模型的一致,實現(xiàn)業(yè)務(wù)語言與代碼語言的統(tǒng)一。
2.2.4 領(lǐng)域劃分及建模
DDD 內(nèi)核的代碼模型來源于領(lǐng)域模型,每個代碼模型的代碼對象跟領(lǐng)域?qū)ο笠灰粚?yīng)。
通過UML類圖(通過顏色標(biāo)注區(qū)分聚合根、實體、值對象等)、用例圖、時序圖完成軟件模型設(shè)計。
2.3 整潔架構(gòu)(洋蔥架構(gòu))
整潔架構(gòu)(Clean Architecture)是由Bob大叔在2012年提出的一個架構(gòu)模型,顧名思義,是為了使架構(gòu)更簡潔。
整潔架構(gòu)最主要原則是依賴原則,它定義了各層的依賴關(guān)系,越往里,依賴越低,代碼級別越高。外圓代碼依賴只能指向內(nèi)圓,內(nèi)圓不知道外圓的任何事情。一般來說,外圓的聲明(包括方法、類、變量)不能被內(nèi)圓引用。同樣的,外圓使用的數(shù)據(jù)格式也不能被內(nèi)圓使用。
整潔架構(gòu)各層主要職能如下:
-
Entities:實現(xiàn)領(lǐng)域內(nèi)核心業(yè)務(wù)邏輯,它封裝了企業(yè)級的業(yè)務(wù)規(guī)則。一個 Entity 可以是一個帶方法的對象,也可以是一個數(shù)據(jù)結(jié)構(gòu)和方法集合。一般我們建議創(chuàng)建充血模型。
-
Use Cases:實現(xiàn)與用戶操作相關(guān)的服務(wù)組合與編排,它包含了應(yīng)用特有的業(yè)務(wù)規(guī)則,封裝和實現(xiàn)了系統(tǒng)的所有用例。
-
Interface Adapters:它把適用于 Use Cases 和 entities 的數(shù)據(jù)轉(zhuǎn)換為適用于外部服務(wù)的格式,或把外部的數(shù)據(jù)格式轉(zhuǎn)換為適用于 Use Casess 和 entities 的格式。
-
Frameworks and Drivers:這是實現(xiàn)所有前端業(yè)務(wù)細(xì)節(jié)的地方,UI,Tools,F(xiàn)rameworks 等以及數(shù)據(jù)庫等基礎(chǔ)設(shè)施。
3. 落地實踐
3.1 概述
在整個DDD開發(fā)過程中,除了建模方法和理論的學(xué)習(xí),實際技術(shù)落地還會遇到很多問題。在多個項目的不斷開發(fā)演進(jìn)過程中,循序漸進(jìn)的總結(jié)了很多經(jīng)驗和小技巧,用于解決過往的缺憾和不足。走向DDD的路有千萬條,這些只是其中的一些可選方案,如有紕漏還請指正。
3.2 工程示例簡介
目前我們采用的是內(nèi)核整體分離,如下圖所示。
b2b-baseproject-kernel 內(nèi)核模塊說明
其中: b2b-baseproject-kernel 為內(nèi)核的Maven工程示例, b2b-baseproject-center為讀寫服務(wù)匯總的中心對外服務(wù)工程示例。
圖3-1 kernel基礎(chǔ)工程示例
內(nèi)核Maven工程模塊說明:
1. b2b-baseproject-kernel-common 常用工具類,常量等,不對外SDK暴露;
2. b2b-baseproject-kernel-export 內(nèi)核對外暴露的信息,為常量,枚舉等,可直接讓外部SDK依賴并對外,減少通用知識重復(fù)定義(可選);
3. b2b-baseproject-kernel-dto 數(shù)據(jù)傳輸層,方便app層和domain層共享數(shù)據(jù)傳輸對象,不對外SDK暴露;
4. b2b-baseproject-kernel-ext-sdk 擴(kuò)展點;(可選,不需要可直接移除)
5. b2b-baseproject-kernel-domain 領(lǐng)域?qū)拥龋ㄒ部梢圆粍澐肿幽K,按需劃分即可);
(b2b-baseproject-kernel-domain-common 通用領(lǐng)域,主要為一些通用值對象;
(b2b-baseproject-kernel-domain-ctxmain 核心領(lǐng)域模型,可自行調(diào)整名稱;
6. b2b-baseproject-kernel-read-app 讀服務(wù)應(yīng)用層;(可選,不需要可直接移除)
7. b2b-baseproject-kernel-app 寫服務(wù)應(yīng)用層;
b2b-baseproject-center 實現(xiàn)模塊說明
圖3-2 center基礎(chǔ)工程示例
center Maven工程模塊說明:
對外SDK
1. b2b-baseproject-sdk 對外sdk工程;
1.1 b2b-baseproject-base-sdk 基礎(chǔ)sdk;
1.2 b2b-baseproject-core-sdk 寫服務(wù)sdk;
1.3 b2b-baseproject-svr-sdk 讀服務(wù)sdk;
基礎(chǔ)設(shè)施
2. b2b-baseproject-center-common 常用工具類,常量等;
3. b2b-baseproject-center-infrastructure 基礎(chǔ)設(shè)施實現(xiàn)層;
(b2b-baseproject-center-dao 基礎(chǔ)設(shè)施層的數(shù)據(jù)庫訪問層,也可不分,直接融合到infrastructure);
(b2b-baseproject-center-es 基礎(chǔ)設(shè)施層的ES訪問層,也可不分,直接融合到infrastructure);
center服務(wù)層
4. b2b-baseproject-center-service center的業(yè)務(wù)服務(wù)層;
接入層
5. b2b-baseproject-center-provider 服務(wù)接入實現(xiàn);
springboot啟動
6. b2b-baseproject-center-bootstrap springboot應(yīng)用啟動層;
備注:對外SDK主要考慮適配CQRS原則,將讀寫分為兩個單獨的module, 如果感覺麻煩,也可以合并為一個SDK對外,用不同的分包隔離即可。
內(nèi)核和實現(xiàn)的關(guān)聯(lián)
使用內(nèi)核和具體實現(xiàn)應(yīng)用分離的劃分是因為前期因為有商業(yè)化衍生出了多版本開發(fā)。當(dāng)然目前架構(gòu)組是不建議一個內(nèi)核多套實現(xiàn)的,而是建議一個內(nèi)核加上一個主版本實現(xiàn)。避免因為多版本實現(xiàn)造成分裂,徒增開發(fā)和維護(hù)成本,改為采用配置和擴(kuò)展點來滿足差異化需求。
目前我們開發(fā)只保持一個主版本,但是工程繼續(xù)使用內(nèi)核分離的方式,即一個內(nèi)核+一個主版本實現(xiàn)。
優(yōu)點:
內(nèi)核和實現(xiàn)代碼完全隔離,得到一個比較干凈存粹的內(nèi)核;
雖萬不得已不建議多版本實現(xiàn),但是萬一要支持多版本,可以直接復(fù)用內(nèi)核;
某種意義上,是一種更合理的分離,保證了內(nèi)核和實現(xiàn)版本的分離,各自關(guān)注各自模塊的核心問題;
缺點:
- 聯(lián)調(diào)成本增加,每次改完需要本地install 或者推送到遠(yuǎn)程Maven倉庫;
基于以上原因,對于小工程不必做以上分離,直接在一個Maven工程中進(jìn)行依賴開發(fā)即可 ,從很多示例教程也是推薦如此。
CQRS(命令與查詢職責(zé)分離)
CQRS 就是讀寫分離,讀寫分離的主要目的是為了提高查詢性能,同時達(dá)到讀、寫解耦。而 DDD 和 CQRS 結(jié)合,可以分別對讀和寫建模。
查詢模型是一種非標(biāo)準(zhǔn)化數(shù)據(jù)模型,它不反映領(lǐng)域行為,只用于數(shù)據(jù)查詢和顯示;命令模型執(zhí)行領(lǐng)域行為,在領(lǐng)域行為執(zhí)行完成后通知查詢模型。
命令模型如何通知到查詢模型呢?如果查詢模型和領(lǐng)域模型共享數(shù)據(jù)源,則可以省卻這一步;如果沒有共享數(shù)據(jù)源,可以借助于發(fā)布訂閱的消息模式通知到查詢模型,從而達(dá)到數(shù)據(jù)最終一致性。
Martin 在 blog 中指出:CQRS 適用于極少數(shù)復(fù)雜的業(yè)務(wù)領(lǐng)域,如果不是很適合反而會增加復(fù)雜度;另一個適用場景是為了獲取高性能的查詢服務(wù)。
對于寫少讀多的共享類通用數(shù)據(jù)服務(wù)(如主數(shù)據(jù)類應(yīng)用)可以采用讀寫分離架構(gòu)模式。單數(shù)據(jù)中心寫入數(shù)據(jù),通過發(fā)布訂閱模式將數(shù)據(jù)副本分發(fā)到多數(shù)據(jù)中心。通過查詢模型微服務(wù),實現(xiàn)多數(shù)據(jù)中心數(shù)據(jù)共享和查詢。
領(lǐng)域與讀模型的聯(lián)系與差異
領(lǐng)域模型(以聚合根為唯一入口)是承載本體變更的核心,其是對業(yè)務(wù)模型的根本建模。若聚合根為每一個普通的人體,聚合根主鍵就是身份證ID。假設(shè)人人生而自由,不受人控制,那么當(dāng)一個人接受到合理命令后進(jìn)行自我屬性變更,然后對外發(fā)送信息。
而視圖層是人體和社會信息的投影,就如我們的教育情況,職業(yè)情況,健康情況等一樣。是對某個時刻對本體信息的投影。
視圖因為基于消息傳播的特性,我們的很多視圖可能是延遲或者不一致的。事例:
1. 你已經(jīng)陽了,而你的健康碼還是綠碼;
2. 你已經(jīng)結(jié)婚,而戶口本還是未婚;
3. 你的結(jié)婚證上聚合了你配偶的信息;
現(xiàn)實世界的不一致已經(jīng)給我們帶來了很多麻煩和困擾,對于IT系統(tǒng)來說也是一樣。視圖的實時更新總是令人神往,但是在分布式系統(tǒng)中面臨諸多挑戰(zhàn)。而為了消除領(lǐng)域模型變更后各種視圖層的延遲和不一致,就需要在消息傳播和更新時機(jī)上做一些優(yōu)化。但是在業(yè)務(wù)處理上,還是需要容忍一定程度的延遲和不一致,因為分布式系統(tǒng)是很難做到100%的準(zhǔn)實時和一致性的。
3.3 問題及解決方案
3.3.1 領(lǐng)域資源注冊中心
背景
一般來講,領(lǐng)域模型不持有倉庫也不不持有其他服務(wù),是一個比較。這就造成領(lǐng)域模型在做一些驗證的時候,僅能進(jìn)行內(nèi)存態(tài)的驗證。對于rpc服務(wù),以及涉及一些重復(fù)性驗證的情況,就顯得無能為力。為了更好的解決這個問題,我們采用了領(lǐng)域模型注冊中心,采用一個單例的類來持有這些服務(wù);
那我們在領(lǐng)域模型中,從數(shù)據(jù)庫重新加載回來的領(lǐng)域模型,不需要通過spring進(jìn)行數(shù)據(jù)封裝,就可以直接使用所依賴的服務(wù)。
基于此,這些服務(wù)必須是無狀態(tài)的,通過輸入領(lǐng)域模型完成數(shù)據(jù)服務(wù)。
/**
* 租戶注冊中心
*
* @author david
* @date 12/12/22
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
@Setter
public class TenantRegistry {
/**
* 倉庫
*/
private TenantRepository tenantRepository;
/**
* 單例
*/
private static TenantRegistry INSTANCE = new TenantRegistry();
/**
* 獲取單例
*
* @return
*/
public static TenantRegistry getInstance() {
return INSTANCE;
}
}
在領(lǐng)域模型進(jìn)行數(shù)據(jù)保存的時候,可用獲取倉庫或者驗證服務(wù)進(jìn)行數(shù)據(jù)驗證。
/**
* 保存數(shù)據(jù)
*/
public void save() {
this.validate();
TenantRepository tenantRepository = TenantRegistry.getInstance().getTenantRepository();
tenantRepository.save(this);
}
3.3.2 內(nèi)核模塊化
一般來講,主站因為服務(wù)的客戶量廣,需求多樣,導(dǎo)致功能及依賴服務(wù)也會很龐大。然后在進(jìn)行商業(yè)化部署的時候,往往只需要其中10%~50%的能力,如果在部署的時候,全量的服務(wù)和領(lǐng)域模型加載意味著需要配置相關(guān)的底層資源和依賴,否則可能啟動異常。
內(nèi)核能力模塊化就顯得尤為重要,目前我們主要利用spring的條件加載實現(xiàn)內(nèi)核模塊化。如下:
/**
* 租戶構(gòu)建工廠
*
* @author david
*/
@Component
@ConditionalOnExpression("${b2b.baseproject.kernel.ability.tenant:true}")
public class TenantInfoFactory {
}
/**
* 租戶應(yīng)用服務(wù)實現(xiàn)
*
* @author david
*/
@Service
@ConditionalOnExpression("${b2b.baseproject.kernel.ability.tenant:true}")
public class TenantAppServiceImpl implements TenantAppService {
}
//其它相關(guān)資源類似,通過@ConditionalOnExpression("${b2b.baseproject.kernel.ability.tenant:true}") 進(jìn)行動態(tài)開關(guān);
這樣在applicaiton.yml 配置相關(guān)能力的true/false, 就可以實現(xiàn)相關(guān)能力的按需加載,當(dāng)然這是強(qiáng)依賴spring的基礎(chǔ)能力情況下。
//appliciaton.yml 配置
b2b:
baseproject:
kernel:
ability:
tenant: true
dict: true
scene: true
可選進(jìn)一步優(yōu)化依賴
條件加載使用了spring的注解,某種意義上導(dǎo)致內(nèi)核和spring進(jìn)行了耦合。然而,項目中總有終極DDD患者,希望內(nèi)核中最好連spring的依賴也去掉。這個時候,可以將spring的裝配專門抽取到一個Maven的module作為starter,由這個starter負(fù)責(zé)spring的相關(guān)的注入和依賴進(jìn)行適配。對于模塊化加載配置,可以繼續(xù)沿用conditional的配置,本質(zhì)上差異不大。
3.3.3 倉庫層diff實踐(可選項)
本案例僅在使用關(guān)系型數(shù)據(jù)庫,且為了提升更新時性能場景適用。如果能偏向于采用支持事務(wù)的NoSQL數(shù)據(jù)庫,那么本實踐可直接略過。
如果不是受制于關(guān)系型數(shù)據(jù)庫的更加流行的制約,在面向DDD開發(fā)之后,大家可能更偏向于NoSQL數(shù)據(jù)庫,可以將領(lǐng)域?qū)ο笠跃酆细臑檎w進(jìn)行整存整取,這樣可以大大的降低倉庫層存取持久化數(shù)據(jù)的開發(fā)量。而現(xiàn)狀是大部分項目都依賴于關(guān)系型數(shù)據(jù)庫,故而很多數(shù)據(jù)依然存在復(fù)雜的數(shù)據(jù)庫存儲關(guān)系。
如果聚合根下關(guān)聯(lián)多個實體,那么在更新的時候,比較簡潔的方式是整體覆蓋,即使數(shù)據(jù)行沒有發(fā)生變更。有時候為了提升數(shù)據(jù)庫更新的性能,就需要按需更新,這時候就需要追蹤實體對象是否發(fā)生變更。
對實體對象的變更追蹤有兩個方式:
A -> 保存更新前快照,使用反射工具深度對比值是否變更;
B -> 使用RecordLog 作為數(shù)據(jù)狀態(tài)跟蹤;
在過往項目中,A/B方案均采用過,A方案的代碼侵入較少,但是需要保留更新前完整快照,使用反射情況下性能會略有影響。 B方案不需要保持更新前完整快照, 也不用反射,但是需要在需要diff的實體對象中增加RecordLog值對象標(biāo)記數(shù)據(jù)是新增、修改、或者未變更。
目前我們主要采用B方案,在涉及實體變更的入口方法,順便調(diào)用RecordLog的更新方法,這樣在倉庫層既可以判斷是新增、修改、還是沒有發(fā)生變更。倉庫層在執(zhí)行保存的時候,則可用通過recordLog值對象的creating, updating判斷數(shù)據(jù)的狀態(tài)。
/**
* 日志值對象,用于記錄數(shù)據(jù)日志信息
*
* @author david
* @date 2020-08-24
*/
@Getter
@Setter
@ToString
@ValueObject
public class RecordLog implements Serializable, RecordLogCompatible {
/**
* 創(chuàng)建人
*/
private String creator;
/**
* 操作人
*/
private String operator;
/**
* 并發(fā)版本號,不一定以第三方傳入的為準(zhǔn)
*/
private Integer concurrentVersion;
/**
* 創(chuàng)建時間,不一定以第三方傳入的為準(zhǔn)
*/
private Date created;
/**
* 修改時間, 不一定以第三方傳入的為準(zhǔn)
*/
private Date modified;
/**
* 創(chuàng)建中
*/
private transient boolean creating;
/**
* 修改中
*/
private transient boolean updating;
/**
* 創(chuàng)建時構(gòu)建
*
* @param creator
* @return
*/
public static RecordLog buildWhenCreating(String creator) {
return buildWhenCreating(creator, new Date());
}
/**
* 創(chuàng)建時構(gòu)建,傳入創(chuàng)建時間
*
* @param creator
* @param createTime
* @return
*/
public static RecordLog buildWhenCreating(String creator, Date createTime) {
RecordLog recordLog = new RecordLog();
recordLog.creator = creator;
recordLog.created = createTime;
recordLog.modified = createTime;
recordLog.operator = creator;
recordLog.concurrentVersion = 1;
recordLog.creating = true;
return recordLog;
}
/**
* 更新
*
* @param operator
*/
public void update(String operator) {
setOperator(operator);
setModified(new Date());
setUpdating(true);
concurrentVersion++;
}
}
// 實體變更的時候,需要同步標(biāo)記recordLog
public class TenantInfo implements AggregateRoot<TenantIdentifier> {
/**
* 失效數(shù)據(jù)
*
* @param operator
*/
public void invalid(String operator) {
setStatus(StatusEnum.NO);
recordLog.update(operator);
}
/**
* 發(fā)布
*
* @param operator
*/
public void publish(String operator) {
setBusinessStatus(TenantBusinessStatusEnum.PUBLISH);
recordLog.update(operator);
}
/**
* 保存到倉庫
*
* @param tenantInfo
*/
@Override
@Transactional
public void save(TenantInfo tenantInfo) {
TenantInfoPO tenantInfoPO = TenantInfoAssembler.convertToPO(tenantInfo);
RecordLog recordLog = tenantInfo.getRecordLog();
//創(chuàng)建diff判斷
if (recordLog.isCreating()) {
tenantInfoMapper.insert(tenantInfoPO);
} else if (recordLog.isUpdating()) { //更新diff判斷
UpdateWrapper<TenantInfoPO> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda().eq(TenantInfoPO::getTenantId, tenantInfoPO.getTenantId());
tenantInfoMapper.update(tenantInfoPO, updateWrapper);
}
//將領(lǐng)域事件轉(zhuǎn)換為taskPo, 并在一個事務(wù)之中保存到數(shù)據(jù)庫,以便保證最終被消費
tenantInfo.publish(localTaskEventFactory.buildEventPersistenceAdapter(event -> TaskAssembler.tenantEventToTaskPO(event)));
}
3.3.4 讀服務(wù)設(shè)計
一個完整的領(lǐng)域服務(wù),只是寫入沒有讀取是不夠的,只寫不讀會出現(xiàn)信息黑洞,導(dǎo)致領(lǐng)域變更無法被外部感知和使用。如前面所述,讀服務(wù)是面向視圖的,其需要的是容易檢索(索引服務(wù)),寬表(冗余關(guān)聯(lián)信息),摘要信息。且讀服務(wù)不對源數(shù)據(jù)進(jìn)行修改,無需進(jìn)行加鎖更注重響應(yīng)快速。
目前內(nèi)核能相對標(biāo)準(zhǔn)化的讀服務(wù),主要針對聚合根進(jìn)行基本的詳情檢索,如通過聚合根主鍵返回基本視圖信息、列表檢索等;其他個性化定制化的查詢參數(shù)和響應(yīng)結(jié)果可以依據(jù)需求自行設(shè)計和擴(kuò)展,如果是比較定制的查詢服務(wù),可以不必落地到內(nèi)核之中。
在b2b-baseproject-kernel工程的 read-app 模塊中,我們定義了讀服務(wù)的接口和約束返回對象,則在實現(xiàn)的center工程中,主要實現(xiàn)底層的讀倉庫和SDK接入層即可(可通過ES, 關(guān)系型數(shù)據(jù)庫, redis 等來提供底層的檢索服務(wù))。
讀服務(wù)接口:
/**
* 租戶應(yīng)用查詢服務(wù)
*
* @author david
**/
public interface TenantInfoQueryService {
/**
* 通過租戶code查詢
*
* @param req
* @return
*/
TenantConstraint getTenantByCode(GetTenantByCodeReq req);
}
/**
* 通過租戶編碼查詢租戶信息請求
*
* @author david
*/
@Setter
@Getter
@ToString
public class GetTenantByCodeReq implements Serializable, Verifiable {
/**
* 租戶編碼
*/
private String tenantCode;
@Override
public void validate() {
Validate.notEmpty(tenantCode, CodeDetailEnum.TENANT);
}
}
/**
* 示例租戶讀服務(wù)約束接口
*
* @author david
* @date 4/15/22
*/
public interface TenantConstraint extends RecordLogCompatible {
/**
* 租戶id
*/
Long getTenantId();
/**
* 租戶id,編碼
*/
Integer getTenantCode();
// ...
}
/**
* 租戶應(yīng)用查詢服務(wù)內(nèi)核實現(xiàn)
*
* @author david
**/
@Service
public class TenantInfoQueryServiceImpl implements TenantInfoQueryService {
//租戶讀倉庫
@Resource
private TenantReadRepo tenantReadRepo;
/**
* 通過租戶id查詢
*
* @param req
* @return
*/
@Override
public TenantConstraint getTenantByCode(GetTenantByCodeReq req) {
req.validate();
return tenantReadRepo.getTenantByCode(req.getTenantCode());
}
//...
}
3.3.5 領(lǐng)域事件發(fā)布
如果不依賴binlog和事務(wù)性消息組件, 為了保證領(lǐng)域事件一定被發(fā)送出去,就需要依賴本地事務(wù)表。我們將領(lǐng)域?qū)ο蟊4婧皖I(lǐng)域事件發(fā)布任務(wù)記錄在一個事務(wù)中得以執(zhí)行。在領(lǐng)域事件推送消息中間件MQ中,在數(shù)據(jù)庫保存完畢后,先主動發(fā)送一次(容許失?。?,如果發(fā)送失敗再等待定時調(diào)度掃描事件表重新發(fā)送。如下圖所示:
一般情況下,領(lǐng)域事件都是在業(yè)務(wù)操作的時候產(chǎn)生,此時我們將領(lǐng)域事件暫存到注冊中心。待入庫的時候,在一個事務(wù)包裹中進(jìn)行保存。發(fā)布者如下所示,如果聚合根需要使用此發(fā)布者事件注冊服務(wù),只需要實現(xiàn)此Publisher接口即可。因為內(nèi)部使用了WeakHashMap 作為容器,如果當(dāng)前對象不再被應(yīng)用,之前注冊的事件列表會被自動回收掉。
/**
* 描述:發(fā)布者接口
*
*/
public interface Publisher {
/**
* 容器
*/
Map<Object, List<DomainEvent>> container = Collections.synchronizedMap(new WeakHashMap<>());
/**
* 注冊事件
*
* @param domainEvent
*/
default void register(DomainEvent domainEvent) {
List<DomainEvent> domainEvents = container.get(this);
if (Objects.isNull(domainEvents)) {
domainEvents = Lists.newArrayListWithCapacity(2);
container.put(this, domainEvents);
}
domainEvents.add(domainEvent);
}
/**
* 獲取事件列表
*
* @return
*/
default List<DomainEvent> getEventList() {
return container.get(this);
}
// 更多代碼...略
}
簡化方案
如果一些簡單的應(yīng)用,不需要使用MQ消息隊列進(jìn)行事件中轉(zhuǎn),也可以將本地事件表的發(fā)送狀態(tài)作為任務(wù)處理狀態(tài)。這樣可以簡化一些網(wǎng)絡(luò)開銷,如在一個應(yīng)用內(nèi),借助guava的EventBus組件完成消息發(fā)布-訂閱機(jī)制。即簡化為:訂閱處理器如果全部執(zhí)行成功,才更新消息表為已發(fā)送(也可以認(rèn)為是已執(zhí)行)。
在實際開發(fā)中,實際上我們很多領(lǐng)域事件都是基于此簡化方案進(jìn)行處理的,因領(lǐng)域事件的部分處理功能簡單,使用簡化方案能節(jié)省很多開發(fā)時間和代碼量。
3.3.6 SAGA事務(wù)
概述
采用DDD之后,雖然還是可以從應(yīng)用層采用基礎(chǔ)的事務(wù)性編程保證本地數(shù)據(jù)庫的事務(wù)性。然而當(dāng)處于微服務(wù)架構(gòu)模式,我們的業(yè)務(wù)常常需要多個跨應(yīng)用的微服務(wù)協(xié)同,采用事務(wù)進(jìn)行一致性保證就顯得鞭長莫及。
即使不采用DDD編程, 我們過往已經(jīng)開始采用Binlog(MySQL的主從同步機(jī)制)或者事務(wù)性消息來實現(xiàn)最終一致性。在越來越流行的微服務(wù)架構(gòu)趨勢下(應(yīng)用資源的分布式特性),通過傳統(tǒng)的事務(wù)ACID(atomicity、consistency、isolation、durability)保證一致性已經(jīng)很難,現(xiàn)在我們通過犧牲原子性(atomicity)和隔離性(Isolation),轉(zhuǎn)而通過保證CD來實現(xiàn)最終一致性。
解決分布式事務(wù),有許多技術(shù)方案如:兩階段提交(XA)、TCC、SAGA。
關(guān)于分布式事務(wù)方案的優(yōu)缺點,有很多論文和技術(shù)文章,為什么選擇SAGA ,正如 Chris Richardson在《微服務(wù)架構(gòu)設(shè)計模式》中所述:
XA對中間件要求很高,跨系統(tǒng)的微服務(wù)更是讓XA鞭長莫及;XA和分布式應(yīng)用天生不匹配;
TCC 對每一個參與方需要實現(xiàn)(Try-confirm-cancel)三步,侵入性較大;
SAGA是一種在微服務(wù)架構(gòu)中維護(hù)數(shù)據(jù)一致性的機(jī)制,它可以避免分布式事務(wù)帶來的問題。通過異步消息來協(xié)調(diào)一系列本地事務(wù),從而維護(hù)多個服務(wù)直接的數(shù)據(jù)一致性;
SAGA理論部分, 可以參考:分布式事務(wù):SAGA模式和Pattern: Saga
SAGA 理論
1987年普林斯頓大學(xué)的Hector Garcia-Molina和Kenneth Salem發(fā)表了一篇Paper Sagas,講述的是如何處理long lived transaction(長活事務(wù))。Saga是一個長活事務(wù)可被分解成可以交錯運(yùn)行的子事務(wù)集合。其中每個子事務(wù)都是一個保持?jǐn)?shù)據(jù)庫一致性的真實事務(wù)。 論文地址:sagas
Saga的組成
-
每個Saga由一系列sub-transaction Ti 組成; (每個Ti是保證原子性提交);
-
每個Ti 都有對應(yīng)的補(bǔ)償動作Ci,補(bǔ)償動作用于撤銷Ti造成的結(jié)果; (Ti如果驗證邏輯且只讀,可以為空補(bǔ)償,即不需要補(bǔ)償);
-
每一個Ti操作在分布式系統(tǒng)中,要求保證冪等性(可重復(fù)請求而不產(chǎn)生臟數(shù)據(jù));
Saga的執(zhí)行順序有兩種:
-
T1, T2, T3, ..., Tn (理想狀態(tài),直接成功);
-
T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n (向前恢復(fù)模式,一般為業(yè)務(wù)失?。?;
-
Saga補(bǔ)償示例: 如果在一個事務(wù)處理中,Ti為發(fā)郵件, Saga不會先保存草稿等事務(wù)提交時再發(fā)送,而是立刻發(fā)送完成。 如果任務(wù)最終執(zhí)行失敗, Ti已發(fā)出的郵件將無法撤銷,Ci操作是補(bǔ)發(fā)一封郵件進(jìn)行撤銷說明。
SAGA有兩種主要的模式,協(xié)同式、編排式。
A 事件協(xié)同式SAGA(Event choreography)
把Saga的決策和執(zhí)行順序邏輯分布在Saga的每個參與方中,他們通過相互發(fā)消息的方式來溝通。
在事件編排方法中,第一個服務(wù)執(zhí)行一個事務(wù),然后發(fā)布一個事件,該事件被一個或多個服務(wù)進(jìn)行監(jiān)聽,這些服務(wù)再執(zhí)行本地事務(wù)并發(fā)布(或不發(fā)布)新的事件。當(dāng)最后一個服務(wù)執(zhí)行本地事務(wù)并且不發(fā)布任何事件時,意味著分布式事務(wù)結(jié)束,或者它發(fā)布的事件沒有被任何 Saga 參與者聽到都意味著事務(wù)結(jié)束。
① 優(yōu)點:
-
避免中央?yún)f(xié)調(diào)器單點故障風(fēng)險;
-
當(dāng)涉及的步驟較少服務(wù)開發(fā)簡單,容易實現(xiàn);
② 缺點:
-
服務(wù)之間存在循環(huán)依賴的風(fēng)險;
-
當(dāng)涉及的步驟較多,服務(wù)間關(guān)系混亂,難以追蹤調(diào)測;
-
參與方需要彼此感知上下耦合關(guān)聯(lián)性,無法做到服務(wù)單元化;
B 命令編排式SAGA(Order Orchestrator)
中央?yún)f(xié)調(diào)器(Orchestrator,簡稱 OSO)以命令/回復(fù)的方式與每項服務(wù)進(jìn)行通信,全權(quán)負(fù)責(zé)告訴每個參與者該做什么以及什么時候該做什么。
① 優(yōu)點:
-
服務(wù)之間關(guān)系簡單,避免服務(wù)間循環(huán)依賴,因為 Saga 協(xié)調(diào)器會調(diào)用 Saga 參與者,但參與者不會調(diào)用協(xié)調(diào)器。
-
程序開發(fā)簡單,只需要執(zhí)行命令/回復(fù)(其實回復(fù)消息也是一種事件消息),降低參與者的復(fù)雜性。
-
易維護(hù)擴(kuò)展,在添加新步驟時,事務(wù)復(fù)雜性保持線性,回滾更容易管理,更容易實施和測試。
② 缺點:
-
中央?yún)f(xié)調(diào)器處理邏輯容易變得龐大復(fù)雜,導(dǎo)致難以維護(hù)。
-
存在協(xié)調(diào)器單點故障風(fēng)險。
命令編排式SAGA示例—— 非訂單聚合提票開票申請
Saga在發(fā)票開票申請的案例如下所示,提票申請被拆分為2個主要的SAGA協(xié)調(diào)器。
① 在接收到【母申請單已經(jīng)創(chuàng)建事件】即觸發(fā)生成協(xié)調(diào)器1調(diào)度——開票申請SAGA協(xié)調(diào)器, 用于參數(shù)驗證、訂單鎖定、占用應(yīng)開金額和數(shù)量、最后按開票規(guī)則拆分為多個子申請單(一個子申請單對一張實際的發(fā)票)。在多個子申請單完成創(chuàng)建后, 會發(fā)布【子申請單已創(chuàng)建】事件。
② 在接收到【子申請單已經(jīng)創(chuàng)建事件】即觸發(fā)生成協(xié)調(diào)器2調(diào)度——子申請單提票SAGA協(xié)調(diào)器, 用于子申請單預(yù)占流水記錄、提交財務(wù)開票、接收財務(wù)狀態(tài)同步子申請單狀態(tài)。
? 使用編排式Saga, 對每一個步驟的調(diào)用也不一定是同步的,也可以發(fā)送處理請求后掛起協(xié)調(diào)處理器,等待異步消息通知。通過消息中間件如MQ收到某個步驟的處理結(jié)果消息,然后再恢復(fù)協(xié)調(diào)器的繼續(xù)調(diào)度。假設(shè)Saga事務(wù)的每個步驟都是異步的,那么編排式協(xié)調(diào)器和事件協(xié)調(diào)器就非常類同,唯一的好處是整個業(yè)務(wù)處理的消息收發(fā)均要通過Saga協(xié)調(diào)器作為中樞。當(dāng)前在哪一步驟,下一步要做什么可以由SAGA協(xié)調(diào)器統(tǒng)一支配。
? 對于一個比較復(fù)雜的長活事務(wù),從業(yè)務(wù)的完整性和排查問題的方便性考慮,我們推薦使用Saga編排式事務(wù)來收斂業(yè)務(wù)的調(diào)度復(fù)雜度,以免在消息發(fā)送接收網(wǎng)絡(luò)中迷失。編排式事務(wù)有時候類似一個狀態(tài)機(jī),當(dāng)前任務(wù)執(zhí)行到哪個步驟,哪個狀態(tài)能夠被保存和復(fù)原,且條理性更加清晰。
? 在編排式Saga事務(wù)中,我們需要使用到eventSource類似的事件記錄,以便記錄每一個步驟的執(zhí)行情況和部分上下文信息。除了手動建表之外(目前我們采用的方案),也有很多成熟的框架可供選擇,如:alibaba的seata,微服務(wù)架構(gòu)設(shè)計模式推薦的eventuate 。
風(fēng)險:
當(dāng)然在使用saga中,還需要考慮隔離性缺失帶來的風(fēng)險,尤其是在交易和金融環(huán)節(jié)。這不是saga能直接解決的問題,這需要通過語義鎖(未提交數(shù)據(jù)加字段鎖,防止臟讀)、交換式更新、版本文件、重讀值等方案進(jìn)行處理。
4. 參考資料
4.1 參考書籍
Domain-Driven Design《領(lǐng)域驅(qū)動設(shè)計》--Eric Evans
MicroServices Patterns《微服務(wù)架構(gòu)設(shè)計模式》 -- Chirs Richardson
《DDD 實戰(zhàn)課》 -- 歐創(chuàng)新
_4.2_網(wǎng)絡(luò)資料
領(lǐng)域模型核心概念:實體、值對象和聚合根
聚合(根)、實體、值對象精煉思考總結(jié)
DDD(Domain-Driven Design)領(lǐng)域驅(qū)動設(shè)計在互聯(lián)網(wǎng)業(yè)務(wù)開發(fā)中的實踐
DDD落地實踐
https://www.jianshu.com/p/91bfc4f21caa
https://www.jianshu.com/p/4a0d89dd7c20
領(lǐng)域驅(qū)動設(shè)計(2) 領(lǐng)域事件、DDD分層架構(gòu)
https://my.oschina.net/lxd6825/blog/5485465
saga分布式事務(wù)_本地事務(wù)和分布式事務(wù)-tencent
作者:京東零售?張世彬文章來源:http://www.zghlxwxcb.cn/news/detail-745906.html
來源:京東云開發(fā)者社區(qū) 轉(zhuǎn)載請注明來源文章來源地址http://www.zghlxwxcb.cn/news/detail-745906.html
到了這里,關(guān)于DDD技術(shù)方案落地實踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!