詳解領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)之事件驅(qū)動(dòng)與CQRS
一、前言:從物流詳情開始
大家對(duì)物流跟蹤都不陌生,它詳細(xì)記錄了在什么時(shí)間發(fā)生了什么,并且數(shù)據(jù)作為重要憑證是不可變的。我理解其背后的價(jià)值有這么幾個(gè)方面:業(yè)務(wù)方可以管控每個(gè)子過程、知道目前所處的環(huán)節(jié);另一方面,當(dāng)需要追溯時(shí)候僅僅通過每一步的記錄就可以回放整個(gè)歷史過程。
我在之前的文章中提出過“軟件項(xiàng)目也是人類社會(huì)生產(chǎn)關(guān)系的范疇,只不過我們所創(chuàng)造的勞動(dòng)成果看不見摸不著而已”。所以我們可以借鑒物流跟蹤的思路來開發(fā)軟件項(xiàng)目,把復(fù)雜過程拆解為一個(gè)個(gè)步驟、子過程、狀態(tài),這和我們事件劃分是一致的,這就是事件驅(qū)動(dòng)的典型案例。
二、領(lǐng)域事件
領(lǐng)域事件(Domain Events)是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(Domain Driven Design,DDD)中的一個(gè)概念,用于捕獲我們所建模的領(lǐng)域中所發(fā)生過的事情。
領(lǐng)域事件本身也作為通用語(yǔ)言(Ubiquitous Language)的一部分成為包括領(lǐng)域?qū)<以趦?nèi)的所有項(xiàng)目成員的交流用語(yǔ)。
比如在前述的跨境物流例子中,貨品達(dá)到保稅倉(cāng)以后需要分派工作人員進(jìn)行分揀分包,那么“貨品已到達(dá)保稅倉(cāng)”便是一個(gè)領(lǐng)域事件。
首先,從業(yè)務(wù)邏輯來說該事件關(guān)系到整個(gè)流程的成功或者失?。煌瑫r(shí)又將觸發(fā)后續(xù)子流程;而對(duì)于業(yè)務(wù)方來說,該事件也是一個(gè)標(biāo)志性的里程碑,代表自己的貨品就快配送到自己手中。
所以通常來說,一個(gè)領(lǐng)域事件具有以下幾個(gè)特征:較高的業(yè)務(wù)價(jià)值,有助于形成完整的業(yè)務(wù)閉環(huán),將導(dǎo)致進(jìn)一步的業(yè)務(wù)操作。這里還要強(qiáng)調(diào)一點(diǎn),領(lǐng)域事件具有明確的邊界。
比如:如果你建模的是餐廳的結(jié)賬系統(tǒng),那么此時(shí)的“客戶已到達(dá)”便不是你關(guān)心的重點(diǎn),因?yàn)槟悴豢赡茉诳蛻舻竭_(dá)時(shí)就立即向?qū)Ψ揭X,而“客戶已下單”才是對(duì)結(jié)賬系統(tǒng)有用的事件。
2.1、建模領(lǐng)域事件
在建模領(lǐng)域事件時(shí),我們應(yīng)該根據(jù)限界上下文中的通用語(yǔ)言來命名事件及屬性。如果事件由聚合上的命令操作產(chǎn)生,那么我們通常根據(jù)該操作方法的名字來命名領(lǐng)域事件。
對(duì)于上面的例子“貨品已到達(dá)保稅倉(cāng)”,我們將發(fā)布與之對(duì)應(yīng)的領(lǐng)域事件
GoodsArrivedBondedWarehouseEvent(當(dāng)然在明確的界限上下文中也可以去掉聚合的名字,直接建模為ArrivedBondedWarehouseEvent,這都是命名方面的習(xí)慣)。
事件的名字表明了聚合上的命令方法在執(zhí)行成功之后所發(fā)生的事情,換句話說待定項(xiàng)以及不確定的狀態(tài)是不能作為領(lǐng)域事件的。
一個(gè)行之有效的方法是畫出當(dāng)前業(yè)務(wù)的狀態(tài)流轉(zhuǎn)圖,包含前置操作以及引起的狀態(tài)變更,這里表達(dá)的是已經(jīng)變更完成的狀態(tài)所以我們不用過去時(shí)態(tài)表示,比如刪除或者取消,即代表已經(jīng)刪除或者已經(jīng)取消。
然后對(duì)于其中的節(jié)點(diǎn)進(jìn)行事件建模。如下圖是文件云端存儲(chǔ)的業(yè)務(wù),我們分別對(duì)預(yù)上傳、上傳完成確認(rèn)、刪除等環(huán)節(jié)建?!斑^去時(shí)”事件,PreUploadedEvent、ConfirmUploadedEvent、RemovedEvent。
2.2、領(lǐng)域事件代碼解讀
package domain.event; import java.util.Date; import java.util.UUID; public class DomainEvent { /** * 領(lǐng)域事件還包含了唯一ID, * 但是該ID并不是實(shí)體(Entity)層面的ID概念, * 而是主要用于事件追溯和日志。 * 如果是數(shù)據(jù)庫(kù)存儲(chǔ),該字段通常為唯一索引。 */ private final String id; /** * 創(chuàng)建時(shí)間用于追溯,另一方面不管使用了 * 哪種事件存儲(chǔ)都有可能遇到事件延遲, * 我們通過創(chuàng)建時(shí)間能夠確保其發(fā)生順序。 */ private final Date occurredOn; public DomainEvent() { this.id = String.valueOf(UUID.randomUUID()); this.occurredOn = new Date(); } }
在創(chuàng)建領(lǐng)域事件時(shí),需要注意2點(diǎn):
- 領(lǐng)域事件本身應(yīng)該是不變的(Immutable);
- 領(lǐng)域事件應(yīng)該攜帶與事件發(fā)生時(shí)相關(guān)的上下文數(shù)據(jù)信息,但是并不是整個(gè)聚合根的狀態(tài)數(shù)據(jù)。例如,在創(chuàng)建訂單時(shí)可以攜帶訂單的基本信息,而對(duì)于用戶更新訂單收貨地址事件AddressUpdatedEvent事件,只需要包含訂單、用戶以及新的地址等信息即可。
public class AddressUpdatedEvent extends DomainEvent { //通過userId+orderId來校驗(yàn)訂單的合法性; private String userId; private String orderId; //新的地址 private Address address; //略去具體業(yè)務(wù)邏輯 }
2.3、領(lǐng)域事件的存儲(chǔ)
事件的不可變性與可追溯性都決定了其必須要持久化的原則,我們來看看常見的幾種方案。
2.3.1、單獨(dú)的EventStore
有的業(yè)務(wù)場(chǎng)景中會(huì)創(chuàng)建一個(gè)單獨(dú)的事件存儲(chǔ)中心,可能是Mysql、Redis、Mongo、甚至文件存儲(chǔ)等。這里以Mysql舉例,business_code、event_code用來區(qū)分不同業(yè)務(wù)的不同事件,具體的命名規(guī)則可以根據(jù)實(shí)際需要。
這里需要注意該數(shù)據(jù)源與業(yè)務(wù)數(shù)據(jù)源不一致的場(chǎng)景,我們要確保當(dāng)業(yè)務(wù)數(shù)據(jù)更新以后事件能夠準(zhǔn)確無(wú)誤的記錄下來,實(shí)踐中盡量避免使用分布式事務(wù),或者盡量避免其跨庫(kù)的場(chǎng)景,否則你就得想想如何補(bǔ)償了。千萬(wàn)要避免,用戶更新了收貨地址,但是AddressUpdatedEvent事件保存失敗。
總的原則就是對(duì)分布式事務(wù)Say No,無(wú)論如何,我相信方法總比問題多,在實(shí)踐中我們總可以想到解決方案,區(qū)別在于該方案是否簡(jiǎn)潔、是否做到了解耦。
# 考慮是否需要分表,事件存儲(chǔ)建議邏輯簡(jiǎn)單 CREATE TABLE `event_store` ( `event_id` int(11) NOT NULL auto increment, `event_code` varchar(32) NOT NULL, `event_name` varchar(64) NOT NULL, `event_body` varchar(4096) NOT NULL, `occurred_on` datetime NOT NULL, `business_code` varchar(128) NOT NULL, UNIQUE KEY (`event id`) ) ENGINE=InnoDB COMMENT '事件存儲(chǔ)表';
2.3.2、與業(yè)務(wù)數(shù)據(jù)一起存儲(chǔ)
在分布式架構(gòu)中,每個(gè)模塊都做的相對(duì)比較小,準(zhǔn)確的說是“自治”。如果當(dāng)前業(yè)務(wù)數(shù)據(jù)量較小,可以將事件與業(yè)務(wù)數(shù)據(jù)一起存儲(chǔ),用相關(guān)標(biāo)識(shí)區(qū)分是真實(shí)的業(yè)務(wù)數(shù)據(jù)還是事件記錄;或者在當(dāng)前業(yè)務(wù)數(shù)據(jù)庫(kù)中建立該業(yè)務(wù)自己的事件存儲(chǔ),但是要考慮到事件存儲(chǔ)的量級(jí)必然大于真實(shí)的業(yè)務(wù)數(shù)據(jù),考慮是否需要分表。
這種方案的優(yōu)勢(shì):數(shù)據(jù)自治;避免分布式事務(wù);不需要額外的事件存儲(chǔ)中心。當(dāng)然其劣勢(shì)就是不能復(fù)用。
2.4、領(lǐng)域事件如何發(fā)布
2.4.1、由領(lǐng)域聚合發(fā)送領(lǐng)域事件
/* * 一個(gè)關(guān)于比賽的充血模型例子 * 貧血模型會(huì)構(gòu)造一個(gè)MatchService,我們這里通過模型來觸發(fā)相應(yīng)的事件 * 本例中略去了具體的業(yè)務(wù)細(xì)節(jié) */ public class Match { public void start() { //構(gòu)造Event.... MatchEvent matchStartedEvent = new MatchStartedEvent(); //略去具體業(yè)務(wù)邏輯 DefaultDomainEventBus.publish(matchStartedEvent); } public void finish() { //構(gòu)造Event.... MatchEvent matchFinishedEvent = new MatchFinishedEvent(); //略去具體業(yè)務(wù)邏輯 DefaultDomainEventBus.publish(matchFinishedEvent); } //略去Match對(duì)象基本屬性 }
2.4.2、事件總線VS消息中間件
微服務(wù)內(nèi)的領(lǐng)域事件可以通過事件總線或利用應(yīng)用服務(wù)實(shí)現(xiàn)不同聚合之間的業(yè)務(wù)協(xié)同。即微服務(wù)內(nèi)發(fā)生領(lǐng)域事件時(shí),由于大部分事件的集成發(fā)生在同一個(gè)線程內(nèi),不一定需要引入消息中間件。但一個(gè)事件如果同時(shí)更新多個(gè)聚合數(shù)據(jù),按照 DDD“一個(gè)事務(wù)只更新一個(gè)聚合根”的原則,可以考慮引入消息中間件,通過異步化的方式,對(duì)微服務(wù)內(nèi)不同的聚合根采用不同的事務(wù)
三、Saga分布式事務(wù)
3.1、Saga概要
我們看看如何使用 Saga 模式維護(hù)數(shù)據(jù)一致性?
Saga 是一種在微服務(wù)架構(gòu)中維護(hù)數(shù)據(jù)一致性的機(jī)制,它可以避免分布式事務(wù)所帶來的問題。
一個(gè) Saga 表示需要更新的多個(gè)服務(wù)中的一個(gè),即Saga由一連串的本地事務(wù)組成。每一個(gè)本地事務(wù)負(fù)責(zé)更新它所在服務(wù)的私有數(shù)據(jù)庫(kù),這些操作仍舊依賴于我們所熟悉的ACID事務(wù)框架和函數(shù)庫(kù)。
模式:Saga
通過使用異步消息來協(xié)調(diào)一系列本地事務(wù),從而維護(hù)多個(gè)服務(wù)之間的數(shù)據(jù)一致性。
請(qǐng)參閱(強(qiáng)烈建議):https://microservices.io/patterns/data/saga.html
Saga與TCC相比少了一步Try的操作,TCC無(wú)論最終事務(wù)成功失敗都需要與事務(wù)參與方交互兩次。而Saga在事務(wù)成功的情況下只需要與事務(wù)參與方交互一次, 如果事務(wù)失敗,需要額外進(jìn)行補(bǔ)償回滾。
- 每個(gè)Saga由一系列sub-transaction Ti 組成;
- 每個(gè)Ti 都有對(duì)應(yīng)的補(bǔ)償動(dòng)作Ci,補(bǔ)償動(dòng)作用于撤銷Ti造成的結(jié)果;
可以看到,和TCC相比,Saga沒有“預(yù)留”動(dòng)作,它的Ti就是直接提交到庫(kù)。
Saga的執(zhí)行順序有兩種:
- success:T1, T2, T3, ..., Tn ;
- failure:T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n;
所以我們可以看到Saga的撤銷十分關(guān)鍵,可以說使用Saga的難點(diǎn)就在于如何設(shè)計(jì)你的回滾策略。
3.2、Saga實(shí)現(xiàn)
通過上面的例子我們對(duì)Saga有了初步的體感,現(xiàn)在來深入探討下如何實(shí)現(xiàn)。當(dāng)通過系統(tǒng)命令啟動(dòng)Saga時(shí),協(xié)調(diào)邏輯必須選擇并通知第一個(gè)Saga參與方執(zhí)行本地事務(wù)。一旦該事務(wù)完成,Saga協(xié)調(diào)選擇并調(diào)用下一個(gè)Saga參與方。
這個(gè)過程一直持續(xù)到Saga執(zhí)行完所有步驟。如果任何本地事務(wù)失敗,則 Saga必須以相反的順序執(zhí)行補(bǔ)償事務(wù)。以下幾種不同的方法可用來構(gòu)建Saga的協(xié)調(diào)邏輯。
3.2.1、協(xié)同式(choreography)
把 Saga 的決策和執(zhí)行順序邏輯分布在 Saga的每一個(gè)參與方中,它們通過交換事件的方式來進(jìn)行溝通。
(引用于《微服務(wù)架構(gòu)設(shè)計(jì)模式》相關(guān)章節(jié))
Order服務(wù)創(chuàng)建一個(gè)Order并發(fā)布OrderCreated事件。
Consumer服務(wù)消費(fèi)OrderCreated事件,驗(yàn)證消費(fèi)者是否可以下訂單,并發(fā)布ConsumerVerified事件。
Kitchen服務(wù)消費(fèi)OrderCreated事件,驗(yàn)證訂單,在CREATE_PENDING狀態(tài)下創(chuàng)建故障單,并發(fā)布TicketCreated事件。
Accounting服務(wù)消費(fèi)OrderCreated事件并創(chuàng)建一個(gè)處于PENDING狀態(tài)的Credit CardAuthorization。
Accounting服務(wù)消費(fèi)TicketCreated和ConsumerVerified事件,向消費(fèi)者的信用卡收費(fèi),并發(fā)布信用卡授權(quán)失敗事件。
Kitchen服務(wù)使用信用卡授權(quán)失敗事件并將故障單的狀態(tài)更改為REJECTED。
訂單服務(wù)消費(fèi)信用卡授權(quán)失敗事件,并將訂單狀態(tài)更改為已拒絕。
3.2.2、編排式(orchestration)
把Saga的決策和執(zhí)行順序邏輯集中在一個(gè)Saga編排器類中。Saga 編排器發(fā)出命令式消息給各個(gè) Saga 參與方,指示這些參與方服務(wù)完成具體操作(本地事務(wù))。類似于一個(gè)狀態(tài)機(jī),當(dāng)參與方服務(wù)完成操作以后會(huì)給編排器發(fā)送一個(gè)狀態(tài)指令,以決定下一步做什么。
(引用于《微服務(wù)架構(gòu)設(shè)計(jì)模式》相關(guān)章節(jié))
我們來分析一下執(zhí)行流程
Order Service首先創(chuàng)建一個(gè)Order和一個(gè)創(chuàng)建訂單控制器。之后,路徑的流程如下:
Saga orchestrator向Consumer Service發(fā)送Verify Consumer命令。
Consumer Service回復(fù)Consumer Verified消息。
Saga orchestrator向Kitchen Service發(fā)送Create Ticket命令。
Kitchen Service回復(fù)Ticket Created消息。
Saga協(xié)調(diào)器向Accounting Service發(fā)送授權(quán)卡消息。
Accounting服務(wù)部門使用卡片授權(quán)消息回復(fù)。
Saga orchestrator向Kitchen Service發(fā)送Approve Ticket命令。
Saga orchestrator向訂單服務(wù)發(fā)送批準(zhǔn)訂單命令。
3.2.3、補(bǔ)償策略
之前的描述中我們說過Saga最重要的是如何處理異常,狀態(tài)機(jī)還定義了許多異常狀態(tài)。如上面的6就會(huì)發(fā)生失敗,觸發(fā)AuthorizeCardFailure,此時(shí)我們就要結(jié)束訂單并把之前提交的事務(wù)進(jìn)行回滾。這里面要區(qū)分哪些是校驗(yàn)性事務(wù)、哪些是需要補(bǔ)償?shù)氖聞?wù)。
一個(gè)Saga由三種不同類型的事務(wù)組成:可補(bǔ)償性事務(wù)(可以回滾,因此有一個(gè)補(bǔ)償事務(wù));關(guān)鍵性事務(wù)(這是 Saga的成敗關(guān)鍵點(diǎn),比如4賬戶代扣);以及可重復(fù)性事務(wù),它不需要回滾并保證能夠完成(比如6更新狀態(tài))。
在Create Order Saga 中,createOrder()、createTicket()步驟是可補(bǔ)償性事務(wù)且具有撤銷其更新的補(bǔ)償事務(wù)。
verifyConsumerDetails()事務(wù)是只讀的,因此不需要補(bǔ)償事務(wù)。authorizeCreditCard()事務(wù)是這個(gè) Saga的關(guān)鍵性事務(wù)。如果消費(fèi)者的信用卡可以授權(quán),那么這個(gè)Saga保證完成。approveTicket()和approveRestaurantOrder()步驟是在關(guān)鍵性事務(wù)之后的可重復(fù)性事務(wù)。
認(rèn)真拆解每個(gè)步驟、然后評(píng)估其補(bǔ)償策略尤為重要,正如你看到的,每種類型的事務(wù)在對(duì)策中扮演著不同的角色。
四、CQRS
前面講述了事件的概念,又分析了Saga如何解決復(fù)雜事務(wù),現(xiàn)在我們來看看CQRS為什么在DDD中廣泛被采用。除了讀寫分離的特征以外,我們用事件驅(qū)動(dòng)的方式來實(shí)踐Command邏輯能有效降低業(yè)務(wù)的復(fù)雜度。
當(dāng)你明白如何建模事件、如何規(guī)避復(fù)雜事務(wù),明白什么時(shí)候用消息中間件、什么時(shí)候采用事件總線,才能理解為什么是CQRS、怎么正確應(yīng)用。
下面是我們項(xiàng)目中的設(shè)計(jì),這里為什么會(huì)出現(xiàn)Read/Write Service,是為了封裝調(diào)用,service內(nèi)部是基于聚合發(fā)送事件。因?yàn)槲野l(fā)現(xiàn)在實(shí)際項(xiàng)目中,很多人都會(huì)第一時(shí)間問我要XXXService而不是XXX模型,所以在DDD沒有完全普及的項(xiàng)目中建議大家采取這種居中策略。這也符合咱們的解耦,對(duì)方依賴我的抽象能力,然而我內(nèi)部是基于DDD還是傳統(tǒng)的流程代碼對(duì)其是無(wú)關(guān)透明的。
我們先來看看事件以及處理器的時(shí)序關(guān)系。
這里還是以文件云端存儲(chǔ)業(yè)務(wù)為例,下面是一些處理器的核心代碼。注釋行是對(duì)代碼功能、用法以及擴(kuò)展方面的解讀,請(qǐng)認(rèn)真閱讀。
package domain; import domain.event.DomainEvent; import domain.handler.event.DomainEventHandler; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class DomainRegistry { private Map<String, List<DomainEventHandler>> handlerMap = new HashMap<String, List<DomainEventHandler>>(); private static DomainRegistry instance; private DomainRegistry() { } public static DomainRegistry getInstance() { if (instance == null) { instance = new DomainRegistry(); } return instance; } public Map<String, List<DomainEventHandler>> getHandlerMap() { return handlerMap; } public List<DomainEventHandler> find(String name) { if (name == null) { return null; } return handlerMap.get(name); } //事件注冊(cè)與維護(hù),register分多少個(gè)場(chǎng)景根據(jù)業(yè)務(wù)拆分, //這里是業(yè)務(wù)流的核心。如果多個(gè)事件需要維護(hù)前后依賴關(guān)系, //可以維護(hù)一個(gè)priority邏輯 public void register(Class<? extends DomainEvent> domainEvent, DomainEventHandler handler) { if (domainEvent == null) { return; } if (handlerMap.get(domainEvent.getName()) == null) { handlerMap.put(domainEvent.getName(), new ArrayList<DomainEventHandler>()); } handlerMap.get(domainEvent.getName()).add(handler); //按照優(yōu)先級(jí)進(jìn)行事件處理器排序 。。。 } }
文件上傳完畢事件的例子。
package domain.handler.event; import domain.DomainRegistry; import domain.StateDispatcher; import domain.entity.meta.MetaActionEnums; import domain.event.DomainEvent; import domain.event.MetaEvent; import domain.repository.meta.MetaRepository; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; /** * @Description:一個(gè)事件操作的處理器 * 我們混合使用了Saga的兩種模式,外層事件交互; * 對(duì)于單個(gè)復(fù)雜的事件內(nèi)部采取狀態(tài)流轉(zhuǎn)實(shí)現(xiàn)。 */ @Component public class MetaConfirmUploadedHandler implements DomainEventHandler { @Resource private MetaRepository metaRepository; public void handle(DomainEvent event) { //1.我們?cè)诋?dāng)前的上下文中定義個(gè)ThreadLocal變量 //用于存放事件影響的聚合根信息(線程共享) //2.當(dāng)然如果有需要額外的信息,可以基于event所 //攜帶的信息構(gòu)造Specification從repository獲取 // 代碼示例 // metaRepository.queryBySpecification(SpecificationFactory.build(event)); DomainEvent domainEvent = metaRepository.load(); //此處是我們的邏輯 。。。。 //對(duì)于單個(gè)操作比較復(fù)雜的,可以使用狀態(tài)流轉(zhuǎn)進(jìn)一步拆分 domainEvent.setStatus(nextState); //在事件觸發(fā)之后,仍需要一個(gè)狀態(tài)跟蹤器來解決大事務(wù)問題 //Saga編排式 StateDispatcher.dispatch(); } @PostConstruct public void autoRegister() { //此處可以更加細(xì)分,注冊(cè)在哪一類場(chǎng)景中,這也是事件驅(qū)動(dòng)的強(qiáng)大、靈活之處。 //避免了if...else判斷。我們可以有這樣的意識(shí),一旦你的邏輯里面充斥了大量 //switch、if的時(shí)候來看看自己注冊(cè)的場(chǎng)景是否可以繼續(xù)細(xì)分 DomainRegistry.getInstance().register(MetaEvent.class, this); } public String getAction() { return MetaActionEnums.CONFIRM_UPLOADED.name(); } //適用于前后依賴的事件,通過優(yōu)先級(jí)指定執(zhí)行順序 public Integer getPriority() { return PriorityEnums.FIRST.getValue(); } }
事件總線邏輯
package domain; import domain.event.DomainEvent; import domain.handler.event.DomainEventHandler; import java.util.List; public class DefaultDomainEventBus { public static void publish(DomainEvent event, String action, EventCallback callback) { List<DomainEventHandler> handlers = DomainRegistry.getInstance(). find(event.getClass().getName()); handlers.stream().forEach(handler -> { if (action != null && action.equals(handler.getAction())) { Exception e = null; boolean result = true; try { handler.handle(event); } catch (Exception ex) { e = ex; result = false; //自定義異常處理 。。。 } finally { //write into event store saveEvent(event); } //根據(jù)實(shí)際業(yè)務(wù)處理回調(diào)場(chǎng)景,DefaultEventCallback可以返回 if (callback != null) { callback.callback(event, action, result, e); } } }); } }
五、自治服務(wù)和系統(tǒng)
DDD中強(qiáng)調(diào)限界上下文的自治特性,事實(shí)上,從更小的粒度來看,對(duì)象仍然需要具備自治的這四個(gè)特性,即:最小完備、自我履行、穩(wěn)定空間、獨(dú)立進(jìn)化。其中自我履行是重點(diǎn),因?yàn)椴粡?qiáng)依賴外部所以穩(wěn)定、因?yàn)榉€(wěn)定才可能獨(dú)立進(jìn)化。這就是六邊形架構(gòu)在DDD中較為普遍的原因。
六、結(jié)語(yǔ)
本文所講述的事件、Saga、CQRS的方案均可以單獨(dú)使用,可以應(yīng)用到你的某個(gè)method、或者你的整個(gè)package。項(xiàng)目中我們并不一定要實(shí)踐一整套CQRS,只要其中的某些思想解決了我們項(xiàng)目中的某個(gè)問題就足夠了。
也許你現(xiàn)在已經(jīng)磨刀霍霍,準(zhǔn)備在項(xiàng)目中實(shí)踐一下這些技巧。不過我們要明白“每一個(gè)硬幣都有兩面性”,我們不僅看到高擴(kuò)展、解耦的、易編排的優(yōu)點(diǎn)以外,仍然要明白其所帶來的問題。利弊分析以后再去決定如何實(shí)現(xiàn)才是正確的應(yīng)對(duì)之道。
- 這類編程模式有一定的學(xué)習(xí)曲線;
- 基于消息傳遞的應(yīng)用程序的復(fù)雜性;
- 處理事件的演化有一定難度;
- 刪除數(shù)據(jù)存在一定難度;
- 查詢事件存儲(chǔ)庫(kù)非常有挑戰(zhàn)性。
不過我們還是要認(rèn)識(shí)到在其適合的場(chǎng)景中,六邊形架構(gòu)以及DDD戰(zhàn)術(shù)將加速我們的領(lǐng)域建模過程,也迫使我們從嚴(yán)格的通用語(yǔ)言角度來解釋一個(gè)領(lǐng)域,而不是一個(gè)個(gè)需求。任何更強(qiáng)調(diào)核心域而不是技術(shù)實(shí)現(xiàn)的方式都可以增加業(yè)務(wù)價(jià)值,并使我們獲得更大的競(jìng)爭(zhēng)優(yōu)勢(shì)。
以上就是詳解領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)之事件驅(qū)動(dòng)與CQRS的詳細(xì)內(nèi)容,更多關(guān)于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì) 事件驅(qū)動(dòng)與CQRS的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring的定時(shí)任務(wù)@Scheduled源碼詳解
這篇文章主要介紹了Spring的定時(shí)任務(wù)@Scheduled源碼詳解,@Scheduled注解是包org.springframework.scheduling.annotation中的一個(gè)注解,主要是用來開啟定時(shí)任務(wù),本文提供了部分實(shí)現(xiàn)代碼與思路,需要的朋友可以參考下2023-09-09Spring Cache相關(guān)知識(shí)總結(jié)
今天帶大家學(xué)習(xí)Spring的相關(guān)知識(shí),文中對(duì)Spring Cache作了非常詳細(xì)的介紹,對(duì)正在學(xué)習(xí)Java Spring的小伙伴們很有幫助,需要的朋友可以參考下2021-05-05jpa多數(shù)據(jù)源時(shí)Hibernate配置自動(dòng)生成表不生效的解決
這篇文章主要介紹了jpa多數(shù)據(jù)源時(shí)Hibernate配置自動(dòng)生成表不生效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02Java實(shí)現(xiàn)的DES加密解密工具類實(shí)例
這篇文章主要介紹了Java實(shí)現(xiàn)的DES加密解密工具類,結(jié)合具體實(shí)例形式分析了Java實(shí)現(xiàn)的DES加密解密工具類定義與使用方法,需要的朋友可以參考下2017-09-09SpringCloud LoadBalancer自定義負(fù)載均衡器使用解析
LoadBalancerClient 是 SpringCloud 提供的一種負(fù)載均衡客戶端,Ribbon 負(fù)載均衡組件內(nèi)部也是集成了 LoadBalancerClient 來實(shí)現(xiàn)負(fù)載均衡,本文給大家深入解析 LoadBalancerClient 接口源碼,感興趣的朋友跟隨小編一起看看吧2023-04-04RestTemplate設(shè)置超時(shí)時(shí)間及返回狀態(tài)碼非200處理
這篇文章主要為大家介紹了RestTemplate設(shè)置超時(shí)時(shí)間及返回狀態(tài)碼非200處理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06SpringBoot單機(jī)限流的實(shí)現(xiàn)
在系統(tǒng)運(yùn)維中, 有時(shí)候?yàn)榱吮苊庥脩舻膼阂馑⒔涌? 會(huì)加入一定規(guī)則的限流,本文主要介紹了SpringBoot單機(jī)限流的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08GC調(diào)優(yōu)實(shí)戰(zhàn)之高分配速率High?Allocation?Rate
這篇文章主要為大家介紹了GC調(diào)優(yōu)之高分配速率High?Allocation?Rate的實(shí)戰(zhàn)示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-01-01Java獲取時(shí)間差(天數(shù)差,小時(shí)差,分鐘差)代碼示例
這篇文章主要介紹了Java獲取時(shí)間差(天數(shù)差,小時(shí)差,分鐘差)代碼示例,使用SimpleDateFormat來實(shí)現(xiàn)的相關(guān)代碼,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11