DDD將研發(fā)者的目光首先聚焦在業(yè)務(wù)本身上倦西,使技術(shù)架構(gòu)和代碼實(shí)現(xiàn)成為軟件建模過(guò)程中的“副產(chǎn)品”。事件風(fēng)暴都能有效地實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)粉铐,從而建立起良好的領(lǐng)域模型及服務(wù)邊界秦躯。
一裆装、領(lǐng)域事件
想一下計(jì)算機(jī)硬件的工作原理哨免,整個(gè)計(jì)算機(jī)的工作過(guò)程其實(shí)就是一個(gè)對(duì)事件的處理過(guò)程昙沦。當(dāng)你點(diǎn)擊鼠標(biāo)、敲擊鍵盤(pán)或者插上U盤(pán)時(shí)采桃,計(jì)算機(jī)便以中斷的形式處理各種外部事件普办。在軟件開(kāi)發(fā)領(lǐng)域徘钥,事件驅(qū)動(dòng)架構(gòu)(Event Driven Architecture,EDA)早已被開(kāi)發(fā)者用于各種實(shí)踐舆驶,典型的應(yīng)用場(chǎng)景比如瀏覽器對(duì)用戶輸入的處理沙廉、消息機(jī)制以及SOA臼节。
1.1 什么是領(lǐng)域事件
領(lǐng)域事件(Domain Events)是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(Domain Driven Design珊皿,DDD)中的一個(gè)概念亮隙,用于捕獲我們所建模的領(lǐng)域中所發(fā)生過(guò)的事情溢吻。領(lǐng)域事件本身也作為通用語(yǔ)言(Ubiquitous Language)的一部分成為包括領(lǐng)域?qū)<以趦?nèi)的所有項(xiàng)目成員的交流用語(yǔ)果元。比如,在用戶注冊(cè)過(guò)程中蝇狼,我們可能會(huì)說(shuō)“當(dāng)用戶注冊(cè)成功之后倡怎,發(fā)送一封歡迎郵件給客戶监署。”钠乏,此時(shí)的“用戶已經(jīng)注冊(cè)”便是一個(gè)領(lǐng)域事件晓避。
當(dāng)然,并不是所有發(fā)生過(guò)的事情都可以成為領(lǐng)域事件暑塑。一個(gè)領(lǐng)域事件必須對(duì)業(yè)務(wù)有價(jià)值彰触,有助于形成完整的業(yè)務(wù)閉環(huán)况毅,也即一個(gè)領(lǐng)域事件將導(dǎo)致進(jìn)一步的業(yè)務(wù)操作。舉個(gè)咖啡廳建模的例子么鹤,當(dāng)客戶來(lái)到前臺(tái)時(shí)將產(chǎn)生“客戶已到達(dá)”的事件味廊,如果你關(guān)注的是客戶接待,比如需要為客戶預(yù)留位置等柠新,那么此時(shí)的“客戶已到達(dá)”便是一個(gè)典型的領(lǐng)域事件,因?yàn)樗鼘⒂糜谟|發(fā)下一步——“預(yù)留位置”操作蕊退;但是如果你建模的是咖啡結(jié)賬系統(tǒng)憔恳,那么此時(shí)的“客戶已到達(dá)”便沒(méi)有多大存在的必要——你不可能在用戶到達(dá)時(shí)就立即向客戶要錢(qián)對(duì)吧钥组,而”客戶已下單“才是對(duì)結(jié)賬系統(tǒng)有用的事件。
1.2 領(lǐng)域事件的應(yīng)用
在DDD中有一條原則:一個(gè)業(yè)務(wù)用例對(duì)應(yīng)一個(gè)事務(wù)点把,一個(gè)事務(wù)對(duì)應(yīng)一個(gè)聚合根屿附,也即在一次事務(wù)中拿撩,只能對(duì)一個(gè)聚合根進(jìn)行操作压恒。但是在實(shí)際應(yīng)用中错邦,我們經(jīng)常發(fā)現(xiàn)一個(gè)用例需要修改多個(gè)聚合根的情況,并且不同的聚合根還處于不同的限界上下文中伦吠。比如毛仪,當(dāng)你在電商網(wǎng)站上買(mǎi)了東西之后芯勘,你的積分會(huì)相應(yīng)增加。這里的購(gòu)買(mǎi)行為可能被建模為一個(gè)訂單(Order)對(duì)象衡怀,而積分可以建模成賬戶(Account)對(duì)象的某個(gè)屬性,訂單和賬戶均為聚合根够委,并且分別屬于訂單系統(tǒng)和賬戶系統(tǒng)怖现。顯然,我們需要在訂單和積分之間維護(hù)數(shù)據(jù)一致性脐雪,通常的做法是在同一個(gè)事務(wù)中同時(shí)更新兩者战秋,但是這會(huì)存在以下問(wèn)題:1)違背DDD中"單個(gè)事務(wù)修改單個(gè)聚合根"的設(shè)計(jì)原則;2)需要在不同的系統(tǒng)之間采用重量級(jí)的分布式事務(wù)(Distributed Transactioin脂信,也叫XA事務(wù)或者全局事務(wù))狰闪;3)在不同系統(tǒng)之間產(chǎn)生強(qiáng)耦合濒生。
通過(guò)引入領(lǐng)域事件,我們可以很好地解決上述問(wèn)題丽声。 總的來(lái)說(shuō)觉义,領(lǐng)域事件給我們帶來(lái)以下好處:1)解耦微服務(wù)(限界上下文);2)幫助我們深入理解領(lǐng)域模型霉撵;3)提供審計(jì)和報(bào)告的數(shù)據(jù)來(lái)源洪囤;4)邁向事件溯源(Event Sourcing)和CQRS等箍鼓。
還是以上面的電商網(wǎng)站為例,當(dāng)用戶下單之后何暮,訂單系統(tǒng)將發(fā)出一個(gè)“用戶已下單”的領(lǐng)域事件,并發(fā)布到消息系統(tǒng)中海洼,此時(shí)下單便完成了。賬戶系統(tǒng)訂閱了消息系統(tǒng)中的“用戶已下單”事件域帐,當(dāng)事件到達(dá)時(shí)進(jìn)行處理肖揣,提取事件中的訂單信息浮入,再調(diào)用自身的積分引擎(也有可能是另一個(gè)微服務(wù))計(jì)算積分,最后更新用戶積分彤断∫准#可以看到睹欲,此時(shí)的訂單系統(tǒng)在發(fā)送了事件之后,整個(gè)用例操作便結(jié)束了窘疮,根本不用關(guān)心是誰(shuí)收到了事件或者對(duì)事件做了什么處理考余。事件的消費(fèi)方可以是賬戶系統(tǒng)楚堤,也可以是任何一個(gè)對(duì)事件感興趣的第三方身冬,比如物流系統(tǒng)岔乔。由此,各個(gè)微服務(wù)之間的耦合關(guān)系便解開(kāi)了嘿歌。值得注意的一點(diǎn)是,此時(shí)各個(gè)微服務(wù)之間不再是強(qiáng)一致性丧凤,而是基于事件的最終一致性。
此外愿待,領(lǐng)域事件也可以用于微服務(wù)內(nèi)部仍侥,此時(shí)不需要消息中間件鸳君,可以是一個(gè)同步過(guò)程,如下
二腿时、事件風(fēng)暴
事件風(fēng)暴是一項(xiàng)團(tuán)隊(duì)活動(dòng)批糟,旨在通過(guò)領(lǐng)域事件識(shí)別出聚合根看铆,進(jìn)而劃分微服務(wù)的限界上下文。在活動(dòng)中否淤,團(tuán)隊(duì)先通過(guò)頭腦風(fēng)暴的形式羅列出領(lǐng)域中所有的領(lǐng)域事件石抡,整合之后形成最終的領(lǐng)域事件集合,然后對(duì)于每一個(gè)事件啰扛,標(biāo)注出導(dǎo)致該事件的命令(Command)隐解,再然后為每個(gè)事件標(biāo)注出命令發(fā)起方的角色煞茫,命令可以是用戶發(fā)起,也可以是第三方系統(tǒng)調(diào)用或者是定時(shí)器觸發(fā)等蚓曼。最后對(duì)事件進(jìn)行分類(lèi)整理出聚合根以及限界上下文炸宵。
2.1 創(chuàng)建領(lǐng)域事件
領(lǐng)域事件應(yīng)該回答“什么人什么時(shí)候做了什么事情”這樣的問(wèn)題土全,在實(shí)際編碼中,可以考慮采用層超類(lèi)型(Layer Supertype)來(lái)包含事件的某些共有屬性:
可以看到瑞凑,領(lǐng)域事件還包含了ID籽御,但是該ID并不是實(shí)體(Entity)層面的ID概念技掏,而是主要用于事件追溯和日志项鬼。另外,由于領(lǐng)域事件描述的是過(guò)去發(fā)生的事情鸠真,我們應(yīng)該將領(lǐng)域事件建模成不可變的(Immutable)龄毡。從DDD概念上講沦零,領(lǐng)域事件更像一種特殊的值對(duì)象(Value Object)。對(duì)于上文中提到的咖啡廳例子序攘,創(chuàng)建“客戶已到達(dá)”事件如下:
在這個(gè)CustomerArrivedEvent事件中,除了繼承自Event的屬性外丈牢,還自定義了一個(gè)與該事件密切關(guān)聯(lián)的業(yè)務(wù)屬性——客戶人數(shù)(customerNumber)——這樣后續(xù)操作便可預(yù)留相應(yīng)數(shù)目的座位了己沛。另外距境,我們將所有屬性以及CustomerArrivedEvent本身都聲明成了final垫桂,并且不向外暴露任何可能修改這些屬性的方法诬滩,這樣便保證了事件的不變性灭将。
2.2 發(fā)布領(lǐng)域事件
推薦的方式是在聚合根中在聚合根中臨時(shí)保存領(lǐng)域事件:
然后在資源庫(kù)repository發(fā)布事件并清空聚合根中的事件空镜。
2.3 聚合更新與事件發(fā)布的原子性
業(yè)務(wù)操作和事件發(fā)布之間應(yīng)該是原子的捌朴,要么全部成功砂蔽,要么全部失敗以“訂單積分”為例察皇,如果客戶下單成功,但是事件發(fā)送失敗矾缓,下游的賬戶系統(tǒng)便拿不到事件嗜闻,導(dǎo)致最終客戶的積分并不增加桅锄。
要保證業(yè)務(wù)操作和事件發(fā)布之間的原子性,最直接的方法便是采用XA事務(wù)翠肘,比如Java中的JTA束倍,這種方式由于其重量級(jí)并不被人們所看好。但是,對(duì)于一些對(duì)性能要求不那么高的系統(tǒng)绪妹,這種方式未嘗不是一個(gè)選擇甥桂。一些開(kāi)發(fā)框架已經(jīng)能夠支持獨(dú)立于應(yīng)用服務(wù)器的XA事務(wù)管理器(如Atomikos和Bitronix),比如Spring Boot作為一個(gè)微服務(wù)框架便提供了對(duì)Atomikos和Bitronix的支持邮旷。
另一個(gè)選項(xiàng)是采用事件表的方式黄选。這種方式首先將事件保存到聚合根所在的數(shù)據(jù)庫(kù)中,由于事件表和聚合根表同屬一個(gè)數(shù)據(jù)庫(kù)婶肩,整個(gè)過(guò)程只需要一個(gè)本地事務(wù)就能完成办陷。然后,在一個(gè)單獨(dú)的后臺(tái)任務(wù)中讀取事件表中未發(fā)布的事件狡孔,再將事件發(fā)布到消息中間件中懂诗。
這種方式需要注意兩個(gè)問(wèn)題:
1)事件發(fā)布后需要在數(shù)據(jù)庫(kù)標(biāo)記成“已發(fā)布”狀態(tài),發(fā)布事件和標(biāo)記“已發(fā)布”之間需要原子性殃恒。建議把事件的消費(fèi)方創(chuàng)建成冪等的,即消費(fèi)方可以多次消費(fèi)同一個(gè)事件而不污染系統(tǒng)數(shù)據(jù)亥鬓。這個(gè)過(guò)程大致為:整個(gè)過(guò)程中事件發(fā)送和數(shù)據(jù)庫(kù)更新采用各自的事務(wù)管理嵌戈,此時(shí)有可能發(fā)生的情況是事件發(fā)送成功而數(shù)據(jù)庫(kù)更新失敗,這樣在下一次事件發(fā)布操作中庵朝,由于先前發(fā)布過(guò)的事件在數(shù)據(jù)庫(kù)中依然是“未發(fā)布”狀態(tài),該事件將被重新發(fā)布到消息系統(tǒng)中侄旬,導(dǎo)致事件重復(fù)儡羔,但由于事件的消費(fèi)方是冪等的,因此事件重復(fù)不會(huì)存在問(wèn)題鉴扫。
2)持久化機(jī)制的選擇。其實(shí)對(duì)于DDD中的聚合根來(lái)說(shuō)莱预,NoSQL是相比于關(guān)系型數(shù)據(jù)庫(kù)更合適的選擇依沮,比如用MongoDB的Document保存聚合根便是種很自然的方式。但是多數(shù)NoSQL是不支持ACID的辜限,也就是說(shuō)不能保證聚合更新和事件發(fā)布之間的原子性。還好毫深,關(guān)系型數(shù)據(jù)庫(kù)也在向NoSQL方向發(fā)展,比如新版本的MySQL(版本5.7)已經(jīng)能夠提供具備N(xiāo)oSQL特征的JSON存儲(chǔ)和基于JSON的查詢鸳址。此時(shí),我們可以考慮將聚合根序列化成JSON格式的數(shù)據(jù)進(jìn)行保存巡球,從而避免了使用重量級(jí)的ORM工具险胰,又可以在多個(gè)數(shù)據(jù)之間保證ACID起便,何樂(lè)而不為榆综?
三、事件風(fēng)暴工作坊
3.1 識(shí)別領(lǐng)域事件
3.2 識(shí)別決策命令