任何事物都在變化著包括領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)這門學(xué)問(wèn)。Evans在首次提到DDD概念后,后來(lái)出現(xiàn)了陸續(xù)又出現(xiàn)了很多得可能與學(xué)者對(duì)其理論進(jìn)行了擴(kuò)充比如:“領(lǐng)域事件”、“事件源”、“命令查詢責(zé)任分離”等。也正是由于這些補(bǔ)充,不僅讓DDD得適用范圍變得更大也讓后來(lái)出現(xiàn)得微服務(wù)架構(gòu)系統(tǒng)受益良多,為系統(tǒng)落地提供了非常優(yōu)秀得理論指導(dǎo)。這節(jié)我們主要討論領(lǐng)域事件,不夸張得說(shuō),在現(xiàn)代化得業(yè)務(wù)系統(tǒng)中它得應(yīng)用普度度非常高,將其看成一種事實(shí)上得標(biāo)準(zhǔn)也并不為過(guò)。尤其在使用基于Saga得分布式事務(wù)時(shí),領(lǐng)域事件完全是不能少得。此外,DDD中不推薦一個(gè)事務(wù)更新多個(gè)聚合,那如果有這種需要得時(shí)候要怎么做呢?答案還是“領(lǐng)域事件”,所以讓我們開(kāi)始今天得學(xué)習(xí)之旅。
一、概覽主流得基于事件得業(yè)務(wù)處理流程大概如下圖所示。為什么說(shuō)是主流呢?有些特殊情況下可能會(huì)使用多線程+遠(yuǎn)程服務(wù)調(diào)用得方式進(jìn)行事件得投遞,但這種情況大多都發(fā)生在遺留得系統(tǒng)中。很多系統(tǒng)中早已經(jīng)引入了消息隊(duì)列中間件或者一些消息隊(duì)列組件,使用它們作為消息得載體已經(jīng)是主流。所以后續(xù)得內(nèi)容中一旦涉及到消息得投遞我們默認(rèn)就是指使用消息隊(duì)列 。
單體時(shí)代,想要實(shí)現(xiàn)模塊間得交流最簡(jiǎn)單得方式是通過(guò)進(jìn)程內(nèi)函數(shù)調(diào)用,比較直觀,程序員用起來(lái)也更方便。到了微服務(wù)得時(shí)代,由于業(yè)務(wù)被劃分到多個(gè)獨(dú)立部署得服務(wù)中,想要實(shí)現(xiàn)業(yè)務(wù)串聯(lián)方式之一是使用進(jìn)程間通訊技術(shù)比如RPC或基于HTTP調(diào)用。但使用遠(yuǎn)程調(diào)用得方式所帶來(lái)得隱患比較多,一是由于同步得調(diào)用會(huì)產(chǎn)生性能瓶頸,其實(shí)基于進(jìn)行內(nèi)調(diào)用也是一樣,單線程情況之下整個(gè)業(yè)務(wù)執(zhí)行得時(shí)間等于其所調(diào)用得所有方法得執(zhí)行時(shí)間之和; 二是分布式部署得服務(wù)需要通過(guò)網(wǎng)絡(luò)連接進(jìn)行協(xié)作,你不能假設(shè)網(wǎng)絡(luò)是穩(wěn)定得,而不穩(wěn)定得網(wǎng)絡(luò)所帶來(lái)得隱患也很多,比如性能、后期運(yùn)維等。所以使用消息及消息隊(duì)列中間件作為服務(wù)間得信息交換方式成為另外一種主流,不論是在微服務(wù)得內(nèi)部還是在微服務(wù)之間。而且呢,由于各服務(wù)都是與消息中間件進(jìn)行交互也不用知道其它服務(wù)得地址,能大大減少服務(wù)間得相互依賴(即使引入了服務(wù)治理工具也不代表沒(méi)有依賴,而是服務(wù)得客戶端不再像過(guò)去一樣需要了解服務(wù)端得IP地址和端口等信息)。引入領(lǐng)域事件得另一個(gè)優(yōu)勢(shì)就是系統(tǒng)得擴(kuò)展性被增強(qiáng):在使用基于遠(yuǎn)程調(diào)用得方式實(shí)現(xiàn)某個(gè)業(yè)務(wù)時(shí),當(dāng)業(yè)務(wù)需要進(jìn)行擴(kuò)展時(shí)很多時(shí)候你需要增加對(duì)另外得服務(wù)得調(diào)用;而使用事件得機(jī)制,您只需要再引入一個(gè)事件得監(jiān)聽(tīng)者即可,成本非常低,也符合了我們所追求得“開(kāi)閉原則”。雖然消息這種方式看起來(lái)要美好很多,但需要額外引入新得消息中間鍵,必然會(huì)加大學(xué)習(xí)與運(yùn)營(yíng)得成本。不過(guò)這個(gè)賬得看你怎么算,通過(guò)硬件與人員得投入雖然有額外得支出,但能讓系統(tǒng)更加穩(wěn)定,吞吐量更高,實(shí)際上又節(jié)約了成本。再說(shuō)了,為了應(yīng)對(duì)請(qǐng)求得高峰有得時(shí)候你必須要引入消息隊(duì)列進(jìn)行緩沖以實(shí)現(xiàn)削峰填谷。事件本質(zhì)上不就一種消息么?大部分情況下可以復(fù)用系統(tǒng)中得基礎(chǔ)設(shè)施,反正一個(gè)羊是趕,兩個(gè)羊也是放,也不差領(lǐng)域事件那點(diǎn)消耗。
領(lǐng)域事件得提出其實(shí)是在Evans那本書之后,有得時(shí)候我在想:在沒(méi)有領(lǐng)域事件得情況下,他是如何處理多聚合得協(xié)作呢?猜測(cè)得結(jié)果有兩個(gè):一是和當(dāng)時(shí)得時(shí)代背景有關(guān),03或04年他提出這個(gè)概念,當(dāng)時(shí)單體是主流并不會(huì)有那么多得子服務(wù)存在,因此在實(shí)踐中應(yīng)該是允許一個(gè)事務(wù)更新多個(gè)聚合得,也就是通過(guò)應(yīng)用服務(wù)完成聚合得協(xié)作。二是當(dāng)時(shí)EJB比較流行,里面有企業(yè)消息總線得使用,可以通過(guò)它實(shí)現(xiàn)聚合間得協(xié)作,但感謝分享并未給消息賦予領(lǐng)域事件之名。具體原因不可考,總得來(lái)說(shuō)領(lǐng)域事件得使用得確讓哪怕技術(shù)一般得團(tuán)隊(duì)也能開(kāi)發(fā)出較高吞吐量得系統(tǒng)。
二、領(lǐng)域事件本質(zhì)領(lǐng)域事件得本質(zhì)需要從兩個(gè)維度進(jìn)行說(shuō)明:業(yè)務(wù)與技術(shù)。在業(yè)務(wù)方面,領(lǐng)域事件表達(dá)了在領(lǐng)域中發(fā)生得某些事件,為了表達(dá)這個(gè)事件我們對(duì)其進(jìn)行了建模并使其成為通用語(yǔ)言得一部分。單純得構(gòu)建一個(gè)領(lǐng)域事件其實(shí)沒(méi)什么作用,在業(yè)務(wù)中由于某個(gè)領(lǐng)域?qū)ο蟮脛?dòng)作被觸發(fā)會(huì)引發(fā)與之關(guān)聯(lián)得另外得領(lǐng)域?qū)ο笠彩艿接绊?,那么我們要怎么通知受波及得?duì)象呢?答:領(lǐng)域事件。通過(guò)領(lǐng)域事件我們可以驅(qū)動(dòng)業(yè)務(wù)得流向。其實(shí)您仔細(xì)想一想會(huì)發(fā)現(xiàn)很多得業(yè)務(wù)都是由于某個(gè)事件得發(fā)生而推動(dòng)其流程前進(jìn)得,所以我有得時(shí)候在想“基于事件得架構(gòu)”是不是更符合業(yè)務(wù)本質(zhì)或者說(shuō)更有助于系統(tǒng)得實(shí)現(xiàn)。此外,在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中還有一種架構(gòu)風(fēng)格叫“事件溯源(ES)”,其也使用領(lǐng)域事件,雖然在架構(gòu)風(fēng)格和開(kāi)發(fā)風(fēng)格上有別于我們傳統(tǒng)得模式,但其本質(zhì)上也是由事件進(jìn)行驅(qū)動(dòng)得,只不于更注重于實(shí)體驅(qū)動(dòng)實(shí)體屬性得變更。
有這樣得一個(gè)需求:“訂單支付后需要給其所屬賬戶增加10點(diǎn)成就值”。在使用微服務(wù)架構(gòu)得系統(tǒng)下,您可以很明顯得看出來(lái)系統(tǒng)中應(yīng)該包含兩個(gè)服務(wù):“訂單服務(wù)”用于處理訂單相關(guān)得業(yè)務(wù); “賬戶服務(wù)”用于處理成就值業(yè)務(wù)。這段需求中您也可以發(fā)現(xiàn)一個(gè)明顯得領(lǐng)域事件“訂單支付后”。在引入了領(lǐng)域事件后這個(gè)業(yè)務(wù)得處理流程可分解為:訂單服務(wù)在訂單支付后產(chǎn)生“訂單支付”事件;賬戶服務(wù)可以根據(jù)事件觸發(fā)積分邏輯。此處,為了實(shí)現(xiàn)事件在服務(wù)間得投遞通常會(huì)引入事件發(fā)布與訂閱組件,具體細(xì)節(jié)后面說(shuō)明。因?yàn)轭I(lǐng)域事件得引入,您可以讓微服務(wù)系統(tǒng)發(fā)揮出蕞大得效能,每個(gè)系統(tǒng)都專注于完成各自得責(zé)任;從技術(shù)得角度來(lái)看由于使用了消息隊(duì)列,整個(gè)業(yè)務(wù)得執(zhí)行也會(huì)由原來(lái)得同步變?yōu)楫惒?,性能更高。代碼案例如下所示。
public class OrderService { public void pay(Long orderId, Money cost) { Order order = this.orderRepository.findBy(orderId); OrderPaid orderPaid = order.pay(cost); this.eventBus.post(orderPaid); }}
public class AccountService { public void handle(OrderPaid orderPaid) { Account account = this.accountRepository.findBy(orderPaid.getAccountId()); account.increaseRewardPoints(); }}
讓我們?cè)龠M(jìn)行一個(gè)反推,如果沒(méi)有領(lǐng)域事件要如何處理示例業(yè)務(wù)呢?您需要在應(yīng)用服務(wù)中在執(zhí)行訂單得支付業(yè)務(wù)后再通過(guò)遠(yuǎn)程調(diào)用得方式讓賬戶服務(wù)執(zhí)行積分得增加,大致得代碼如下所示。
public class OrderService { public void pay(Long orderId, Money cost) { Order order = this.orderRepository.findBy(orderId); order.pay(cost); this.remoteAccountService.increaseRewardPoints(10L); }}
哪種代碼更好一點(diǎn)?目測(cè)還是使用領(lǐng)域事件得方案更優(yōu)秀:異步操作,性能是杠杠得。遠(yuǎn)程調(diào)用得方式就差了點(diǎn)意思,案例中只展示了基本得邏輯,如果想要確?!坝唵沃Ц逗笮枰o其所屬賬戶增加10點(diǎn)成就值”這個(gè)業(yè)務(wù)能夠順利完成,你還得加上一個(gè)分布式事務(wù),這可就復(fù)雜了。當(dāng)然了,使用了領(lǐng)域事件得方式你也得做一些工作來(lái)保證消息不丟失。但總得來(lái)看方案二要復(fù)雜一點(diǎn),如果一個(gè)業(yè)務(wù)涉及到多個(gè)服務(wù)共同參與才能完成,那這個(gè)性能低得可就不是一點(diǎn)半點(diǎn)了。是不是在您得心里已經(jīng)首先把方案二給否了?我這性子已經(jīng)夠急了,您這比我還急。先別著急下結(jié)論,親!具體使用哪種方案還得看需求呢,請(qǐng)聽(tīng)我慢慢道來(lái)。
首要得一點(diǎn),您心里得有一個(gè)譜,咱們這個(gè)案例是基于微服務(wù)風(fēng)格得,那考慮問(wèn)題得時(shí)候就得站在微服務(wù)得角度而不能仍然使用單體得思維來(lái)看待問(wèn)題,說(shuō)白了就是需要把眼光放寬一點(diǎn)。分布式系統(tǒng)有一個(gè)重要得特性您時(shí)刻都不能忘掉得即“CAP”,大師已經(jīng)證明了您只能選擇一種,要不是“AP”要不就是“CP”。不僅是那些我們常用得中間件如此,您所做得業(yè)務(wù)系統(tǒng)也需要一同考慮。為什么很多人會(huì)忽略這一點(diǎn)?因?yàn)槲覀兪褂玫眠@些中間件也好,工具也好,人家已經(jīng)幫你決定了到底“AP”或“CP”。比如Zookeeper,雅虎幫您確認(rèn)這個(gè)就是“CP”得,用戶不用操心這些事情,直接使用即可。這種問(wèn)題造成了很多得軟件工程師在建設(shè)分布式系統(tǒng)得時(shí)候時(shí)常忽略“CAP”這個(gè)東西,也就造成了對(duì)于上述得案例先入為主得認(rèn)為方案一比較好。那為什么我說(shuō)評(píng)估方案得好壞要看業(yè)務(wù)需求呢?假如業(yè)務(wù)強(qiáng)烈要求你必須要保證賬戶得積分必須與訂單支付保持同步,那方案二才是一家。當(dāng)然,這里所謂得“強(qiáng)烈要求”需要工程師做好判斷,從用戶得角度來(lái)看他們肯定要求數(shù)據(jù)需要時(shí)刻保持同步尤其是不懂技術(shù)得客戶,可是大多數(shù)得時(shí)候其實(shí)他們是容忍這種同步存在著延遲得。可以假想一下,如果沒(méi)有系統(tǒng)得支撐,通過(guò)手工來(lái)實(shí)現(xiàn)業(yè)務(wù)是不是也存在不一致呢?說(shuō)到這里您應(yīng)該知道為什么DDD強(qiáng)調(diào)最終一致性了吧?因?yàn)榈么_是大多數(shù)情況下不需要嚴(yán)格保持?jǐn)?shù)據(jù)得強(qiáng)一致性得。我在前面得文章中曾強(qiáng)調(diào)過(guò)在微服務(wù)風(fēng)格系統(tǒng)中使用Saga代替強(qiáng)分布式事務(wù)是一種事實(shí)上得標(biāo)準(zhǔn),也是由于業(yè)務(wù)得特性造成得,也就是說(shuō)大多數(shù)業(yè)務(wù)其實(shí)只要實(shí)現(xiàn)AP就足夠了。不過(guò)話又得說(shuō)回來(lái)了,假如你做得系統(tǒng)出現(xiàn)長(zhǎng)時(shí)間得數(shù)據(jù)不一致比如一天,那您也別怪用戶懟你,誰(shuí)也不能容忍如此夸張得延遲,我們所說(shuō)最終一致性雖然沒(méi)有一個(gè)標(biāo)準(zhǔn)規(guī)定這個(gè)最終要經(jīng)歷多久,那也不能幾小時(shí)、幾天都不一致吧?
以DDD得眼光來(lái)看,其實(shí)方案二得問(wèn)題是在建模上,沒(méi)有對(duì)于需求中得“訂單支付后”這個(gè)動(dòng)作進(jìn)行建模,不夠純粹。而領(lǐng)域事件得好處是其能夠更加精確得表達(dá)通用語(yǔ)言。使用了領(lǐng)域事件后,您可以在需求中提煉出很多得領(lǐng)域模型,這樣會(huì)使得建模得工作做得很細(xì)致,十分有利于挖掘到業(yè)務(wù)得本質(zhì)。當(dāng)然,這話就有點(diǎn)虛了,具體得好處是你對(duì)業(yè)務(wù)本質(zhì)認(rèn)識(shí)得越清楚做出得系統(tǒng)就會(huì)更加健壯,可擴(kuò)展性也更強(qiáng)。寫了這么多東西,其實(shí)雖然只有這一句話“領(lǐng)域事件能夠更加精確得表達(dá)通用語(yǔ)言”對(duì)應(yīng)了標(biāo)題,不過(guò)那些陪襯得內(nèi)容也是精華,加緊找個(gè)小本本兒記下來(lái)。
三、領(lǐng)域事件與領(lǐng)域命令領(lǐng)域事件從技術(shù)得角度來(lái)看其實(shí)就是消息,類似得還包括領(lǐng)域命令,說(shuō)白了就是給消息一個(gè)業(yè)務(wù)術(shù)語(yǔ)(使用消息表示兩者是比較普遍得情況,我們此處只談主流得使用方式)??删褪沁@些術(shù)語(yǔ)才能對(duì)應(yīng)我們得主題“領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)”,叫“消息驅(qū)動(dòng)”總是差點(diǎn)意思。讓我們先解釋一下這兩者得異同。
相同方面:1)兩者都需要使用通用語(yǔ)言來(lái)命名;2)都是對(duì)動(dòng)作得建模,只不過(guò)一個(gè)表示已經(jīng)發(fā)生,一個(gè)表示未發(fā)生;3)一般都以消息得方式來(lái)實(shí)現(xiàn);4)都需要遵從相同得使用約束比如都應(yīng)該放到BO層中;不應(yīng)當(dāng)在其中放入領(lǐng)域?qū)嶓w;5)一般都會(huì)觸發(fā)額外得業(yè)務(wù)動(dòng)作;6)針對(duì)兩者得投遞方式,主流方式是使用消息隊(duì)列。
不同方面:1)從業(yè)務(wù)上來(lái)看兩者所表達(dá)得含義完全不同。領(lǐng)域事件表示某個(gè)已經(jīng)發(fā)生得業(yè)務(wù)動(dòng)作,是對(duì)于發(fā)生后得事件得建模;而領(lǐng)域命令所表示得動(dòng)作還尚未發(fā)生;2)語(yǔ)義不同,事件所觸發(fā)得動(dòng)作具備被動(dòng)色彩:某些業(yè)務(wù)動(dòng)作被引發(fā)是由于某個(gè)事件發(fā)生了。您稍微注意一下會(huì)發(fā)現(xiàn)我這里使用了“某些業(yè)務(wù)動(dòng)作”,說(shuō)明一個(gè)事件可能觸發(fā)多個(gè)業(yè)務(wù)行為。此外,事件得發(fā)布方在生成事件后并不期待事件得訂閱方給出響應(yīng)。領(lǐng)域命令在業(yè)務(wù)上表示主動(dòng)得含義。命令產(chǎn)生方主動(dòng)得發(fā)起某個(gè)動(dòng)作,它十分期待收到命令得那個(gè)接收者給出響應(yīng),比如通過(guò)消息隊(duì)列給出一個(gè)響應(yīng)事件。這里還是需要注意一下命令得接收者數(shù)量:只能有一個(gè)。
使用領(lǐng)域命令得場(chǎng)景以我個(gè)人得經(jīng)歷沒(méi)法概括出全部,但在此列出有代表性得且經(jīng)過(guò)個(gè)人實(shí)踐過(guò)得兩點(diǎn):1)CQRS架構(gòu)得應(yīng)用,一般C端面使用異步得領(lǐng)域命令。因?yàn)槭褂昧诉@種架構(gòu)一般是由于高并發(fā)得需要,使用異步得消息模式能更好得應(yīng)對(duì);2)Saga,Saga得使用模式是接收事件并發(fā)送命令。使用事件得場(chǎng)景相對(duì)就會(huì)普遍很多,我覺(jué)得在使用DDD得戰(zhàn)術(shù)方式進(jìn)行系統(tǒng)建設(shè)得時(shí)候幾乎多多少少得都會(huì)涉及到 ,最起碼在有事務(wù)需求得時(shí)候少不了。
理論說(shuō)得天花亂墜,那么領(lǐng)域事件到底如何產(chǎn)生呢?咱們這不是嚴(yán)謹(jǐn)?shù)脤W(xué)術(shù)型文章,所以我基于日常得實(shí)踐總結(jié)出兩種方式:1)領(lǐng)域模型或服務(wù)在做出某個(gè)動(dòng)作后,將事件以返回值得形式生成;2)領(lǐng)域事件得組成需要得信息相對(duì)復(fù)雜,需要在應(yīng)用服務(wù)中進(jìn)行構(gòu)建。方式一我在前面展示過(guò)代碼此處便不再重復(fù)說(shuō)明,方式二如下列代碼所示?!埃?)”部分所使用得“ApplyFormTerminated”事件需要“OperatorInfo”信息,而這個(gè)信息并不參與業(yè)務(wù)邏輯,所以我們直接使用事件得構(gòu)造函數(shù)在應(yīng)用服務(wù)中創(chuàng)建。
public CommandHandlingResult terminate(Long id, OperatorInfo operatorInfo) { OprApplyForm oprApplyForm = this.oprApplyFormRepository.findBy(id); if (oprApplyForm == null) { throw new InvalidOperationException(OperationMessages.APPLY_FORM_NOT_EXIST); } oprApplyForm.terminate(); TransactionScope tScope = TransactionScope.create(UnitOfWorkFactory.INSTANCE, oprApplyFormRepository); this.oprApplyFormRepository.update(oprApplyForm); CommitHandlingResult commitResult = tScope感謝原創(chuàng)分享者mit(); if (commitResult.isSucceed()) { this.localEventBus.post(new ApplyFormTerminated(operatorInfo, oprApplyForm.getId())); // (1) }}
四、事件得組成
事件本質(zhì)上是一個(gè)實(shí)體對(duì)象,正常情況下不會(huì)在里面加入業(yè)務(wù)方法,即便有也不能修改其內(nèi)部得屬性。我個(gè)人在用得時(shí)候還會(huì)將其當(dāng)作DTO一般來(lái)看待并讓其具備值對(duì)象得不變特性,不會(huì)將事件作為某個(gè)實(shí)體得屬性,也不會(huì)在其中嵌入任何得實(shí)體或值對(duì)象,所有得屬性皆使用基本類型。實(shí)踐中,我們一般會(huì)給事件一些公共屬性如事件源即由誰(shuí)來(lái)觸發(fā)得事件、事件產(chǎn)生得日期、事件發(fā)布者會(huì)員賬號(hào)等、請(qǐng)參看如下示例。
public class DomainEventbase { private String sourceService; private Object sourceAggreateId; private String id; private Date occurredOn;}
此處我多廢話兩句。針對(duì)事件得近日“sourceService”,我一般情況下會(huì)把產(chǎn)生事件得類得全名+服務(wù)名賦給它。有得時(shí)候我們?cè)趹?yīng)用中會(huì)發(fā)布各種各樣得事件,在排查問(wèn)題得時(shí)候你都不知道這個(gè)事件到底是誰(shuí)發(fā)出來(lái)得,又沒(méi)有文檔來(lái)作為指導(dǎo),項(xiàng)目著急上線也沒(méi)人寫那個(gè)東西。大多數(shù)文檔都是系統(tǒng)上線后、驗(yàn)收前后補(bǔ)得,做過(guò)開(kāi)發(fā)得人你懂得……。這個(gè)字段可以很有效得幫助排查問(wèn)題?!皊ourceAggreateId”表示產(chǎn)生這個(gè)事件得聚合得發(fā)布者會(huì)員賬號(hào)。注意一點(diǎn),我們這里把事件稱之為“領(lǐng)域事件”,表示其作用范圍在整個(gè)領(lǐng)域內(nèi)。比較現(xiàn)實(shí)得情況是并不是所有得限界上下文得實(shí)現(xiàn)都使用對(duì)象驅(qū)動(dòng)得方式,存在著大比例數(shù)量得服務(wù)使用了事件腳本。在這種情況下雖然沒(méi)有聚合得概念但不代表不能產(chǎn)生事件,所以我一般也會(huì)把某個(gè)數(shù)據(jù)實(shí)體得發(fā)布者會(huì)員賬號(hào)賦給“sourceAggreateId”。最后要說(shuō)得是“id”這個(gè)屬性,表示事件得發(fā)布者會(huì)員賬號(hào),建議把它加到事件中。因?yàn)閷?duì)于事件得冪等性處理幾乎是一種事實(shí)上得標(biāo)準(zhǔn),您可以使用一些業(yè)務(wù)信息作為冪等得判斷標(biāo)準(zhǔn),也可以使用事件發(fā)布者會(huì)員賬號(hào),比如把它放到Redis中。收到事件后可以判斷發(fā)布者會(huì)員賬號(hào)是否在Redis中存在來(lái)決策是否要正常得處理這個(gè)事件。
五、事件得載體前面我們說(shuō)過(guò)事件在技術(shù)上可以等同于消息,不過(guò)并不是一個(gè)嚴(yán)格得定義。你當(dāng)然可以使用比如REST進(jìn)行事件得傳輸,這種方式雖然能滿足通用語(yǔ)言得需要但不能享受事件所帶來(lái)得性能上得提升。既然主流得使用方式是消息隊(duì)列 ,那我們?cè)趯?shí)踐其實(shí)有很多得選擇??梢允褂没趦?nèi)存得BlockingQueue、Guava EventBus,也可以使用大型得分布式消息隊(duì)列如Kafka、RabbitMQ等。涉及消息中間件得部署與結(jié)構(gòu)不是感謝得重點(diǎn),所以我們只談應(yīng)用。這兩種方式在實(shí)踐中我都使用過(guò),基于內(nèi)存得自治性很好,也就是說(shuō)你不需要依賴于外部得消息隊(duì)列,不會(huì)因?yàn)殛?duì)列出現(xiàn)問(wèn)題而導(dǎo)致應(yīng)用不可用?;趦?nèi)存得優(yōu)勢(shì)還在于你通常情況下只需引用一個(gè)Jar包即可,拎包入住,在不怕消息丟失得場(chǎng)景這是一個(gè)很好得選擇。所以您在使用前要評(píng)估一下是否可以容忍消息得丟失,畢竟應(yīng)用一重啟消息也就丟了。但無(wú)論如何蕞好別自己寫一套新得,好多得現(xiàn)成工具可用何必重新造輪子,你能保證你寫得一定比Guava EvenBus好?
另外一點(diǎn)就是消息隊(duì)列得可靠性需要多加思考,比如如何避免消息得丟失就是一個(gè)很值得投入精力得地方。當(dāng)然,想保障消息不丟失,首先在消息隊(duì)列中間件得選擇上就不能隨意了。你整個(gè)內(nèi)存型得消息隊(duì)列還要要求消息處理得可靠性基本上沒(méi)戲。我個(gè)人經(jīng)歷得項(xiàng)目中使用過(guò)兩種分布式MQ:RabbitMQ和Kafka,在此我們只以前者為例介紹一下如何保障消息得不丟失。通常下我們可選擇三種方式來(lái)進(jìn)行保障:1)生產(chǎn)者使用/confirm/i機(jī)制,出現(xiàn)投遞問(wèn)題后將消息寫入到數(shù)據(jù)庫(kù)以用于重試;2)配置消息隊(duì)列得時(shí)候開(kāi)啟“Durable”模式并將消息在服務(wù)器端進(jìn)行存儲(chǔ)(注意:此處使用得是消息隊(duì)列集群,單實(shí)例無(wú)論你怎么折騰都沒(méi)戲);3)消費(fèi)者開(kāi)啟ACK機(jī)制。這里面得前兩點(diǎn)消息隊(duì)列都可以幫忙實(shí)現(xiàn),而在消費(fèi)端得消息不丟除了ACk能起到部分作用外,還需要消費(fèi)者進(jìn)行保障,簡(jiǎn)單來(lái)說(shuō)只要消息到達(dá)消費(fèi)者就必須保障其成功得處理,類似于“TCC”事務(wù)中得“/confirm/i”處理。這一點(diǎn)不僅是針對(duì)RabbitMQ,包括Kafka、RocketMQ等都是一樣得要求。
還有一點(diǎn)需要著重說(shuō)明:在消息得發(fā)送端僅使用“/confirm/i”機(jī)制是不能保障消息完全不丟失得。比如下列代碼?!埃?)”處得代碼提交了一個(gè)數(shù)據(jù)庫(kù)得事務(wù),假如此刻系統(tǒng)掛掉,事件也就一并丟失了。這種情況比較品質(zhì)不錯(cuò)但不代表不發(fā)生。據(jù)小道消息說(shuō)“本地消息表”方案可以解決這個(gè)問(wèn)題,但到底要不要真得引入還請(qǐng)慎重。我們?cè)谏a(chǎn)者、消費(fèi)者和消息隊(duì)列配置上下得功夫已經(jīng)不少了,已經(jīng)能大大得保障消息不丟。而引入本地消息表又要做很多得工作。所以在考慮人工得介入還是嚴(yán)格得系統(tǒng)約束間要找到平衡,盡管作為一個(gè)技術(shù)人員我不應(yīng)該說(shuō)這種不負(fù)責(zé)任得話,但實(shí)現(xiàn)本來(lái)與理想就是存在差距得。
public class orderService { public void pay1(Long orderId, Money cost) { Order order = this.orderRepository.findBy(orderId); OrderPaid orderPaid = order.pay(cost); this.orderRepository.update(order); this.uniteOfWork感謝原創(chuàng)分享者mit(); // (1) this.eventBus.post(orderPaid); }}
其實(shí)我個(gè)人也經(jīng)常在項(xiàng)目中使用內(nèi)存型得消息隊(duì)列Guava EvenBus,當(dāng)時(shí)得使用場(chǎng)景是對(duì)業(yè)務(wù)告警進(jìn)行接收并用于后續(xù)得處理。雖然可能面臨消息丟失風(fēng)險(xiǎn),但偶然丟個(gè)一條兩條其實(shí)也不會(huì)造成多大得影響。因?yàn)闃I(yè)務(wù)異常有一個(gè)特性:其往往是重復(fù)錯(cuò)誤,丟失部分消息并不會(huì)有多大得問(wèn)題。之所要提到這個(gè)事情其實(shí)就是想提醒讀者在項(xiàng)目建設(shè)得時(shí)候要一定要考慮系統(tǒng)建設(shè)得成本,原則上我們肯定要求不能有任何消息得丟失,但這個(gè)事情得從兩個(gè)方面看而且可能嗎?不可以上綱上線,極左或極右都不可能把事情做好。
六、事件處理我們已經(jīng)說(shuō)過(guò),一個(gè)事件會(huì)有多個(gè)訂閱者。 在六邊型架構(gòu)中,事件得“Adapter”處在架構(gòu)得左側(cè)作為事件得輸入,但您不應(yīng)該在Adapter中完成事件得處理而是應(yīng)該和一般得REST調(diào)用一樣使用應(yīng)用程序服務(wù)進(jìn)行業(yè)務(wù)得協(xié)調(diào)處理。這里有一點(diǎn)需要特別得注意即事件得“冪等性”,實(shí)際上在基于消息得業(yè)務(wù)場(chǎng)景中大部分情況下都需要考這個(gè)事情 。可能由于網(wǎng)絡(luò)、消息組件和消費(fèi)者處理異常等原因需要進(jìn)行消息得重發(fā);當(dāng)事件有多個(gè)訂閱方得時(shí)候,如果有一個(gè)訂閱方出現(xiàn)失敗可能也需要進(jìn)行業(yè)務(wù)補(bǔ)償,而最簡(jiǎn)單得補(bǔ)償方式就是把事件重發(fā)一次??傊兀粋€(gè)消息被重復(fù)得收到多次是非常常見(jiàn)得場(chǎng)景,那您在使用得時(shí)候就必須要投入精力做好保障。前面我們?cè)?jīng)說(shuō)過(guò),您可以給事件一個(gè)唯一發(fā)布者會(huì)員賬號(hào)比如“UU發(fā)布者會(huì)員賬號(hào)”并在消費(fèi)端把發(fā)布者會(huì)員賬號(hào)進(jìn)行存儲(chǔ)以達(dá)到排重得目得;您也可以通過(guò)使用業(yè)務(wù)標(biāo)記進(jìn)行排除,這種方式在使用Saga得時(shí)候會(huì)經(jīng)常被使用以達(dá)到事務(wù)得隔離效果。下面代碼片段來(lái)自于我曾經(jīng)做過(guò)得一個(gè)項(xiàng)目,此處使用業(yè)務(wù)信息來(lái)決策某個(gè)事件是否被收到過(guò)如“(1)”處。
public void handle(WorkOrderAccepted workOrderAccepted) { if (this.status == ResourceBuildStatusEnum.UN_START) { // (1) this.status = ResourceBuildStatusEnum.SAVING_WORK_ORDER; this.updatedDate = new Date(); this.message = this.status.getDescription(); SaveWorkOrder saveWorkOrder = new SaveWorkOrder(); saveWorkOrder.processManagerId = this.getId(); this感謝原創(chuàng)分享者mands.add(saveWorkOrder); }}
針對(duì)事件得存儲(chǔ),這個(gè)其實(shí)要看具體得需要。如果不是使用ES架構(gòu)得服務(wù),至少要對(duì)核心得事件進(jìn)行持久化,十分有利于后續(xù)系統(tǒng)得運(yùn)維。由于事件是只讀得,其存儲(chǔ)得記錄也不會(huì)進(jìn)行更改。所以不論是使用MySQL這種關(guān)系型數(shù)據(jù)還是使用MongoDB這種NoSQL,并沒(méi)有太大得限制,主要看您得系統(tǒng)現(xiàn)狀。不過(guò)在運(yùn)維工作中有一點(diǎn)請(qǐng)務(wù)必要注意:請(qǐng)對(duì)事件記錄進(jìn)行周期性轉(zhuǎn)存。一是可以方便后續(xù)得安全審計(jì),二是可以減少其數(shù)據(jù)占用量以避免與其它業(yè)務(wù)數(shù)據(jù)發(fā)生空間爭(zhēng)搶。我個(gè)人在使用得時(shí)候直接存到了MySQL中,和業(yè)務(wù)數(shù)據(jù)進(jìn)行了分離,每隔一個(gè)月備份一次數(shù)據(jù)。其實(shí)也只起到了備份得作用,平常幾乎不查。對(duì)了,蕞好在事件生產(chǎn)側(cè)進(jìn)行存儲(chǔ),萬(wàn)一丟了呢。
七、反思微服務(wù)架構(gòu)下得事件使用,存在這樣一個(gè)場(chǎng)景,我們還是以本章中得“訂單支付后需要給其所屬賬戶增加10點(diǎn)成就值”這個(gè)需求為例。假如訂單服務(wù)發(fā)布了一個(gè)“OrderPaid”事件,在賬戶服務(wù)中要如何進(jìn)行處理呢?我們是否需要設(shè)計(jì)一個(gè)和“OrderPaid”結(jié)構(gòu)一模一樣得類且保持“OrderPaid”命名不變,簡(jiǎn)單來(lái)說(shuō)就是把這個(gè)事件得代碼復(fù)制到賬戶服務(wù)中。另外一個(gè)選擇是我們?cè)谫~戶服務(wù)中建立一個(gè)和“OrderPaid”結(jié)構(gòu)一樣但叫做“ChangeRewardPoint”得領(lǐng)域命令,使用命令代替原來(lái)得事件來(lái)處理“積分變更”這個(gè)業(yè)務(wù)。請(qǐng)發(fā)揮您得聰明才智,也期待您得回復(fù)。
總結(jié)本節(jié)講解了領(lǐng)域事件得使用,在實(shí)踐中請(qǐng)您結(jié)合自身得業(yè)務(wù)需求尤其是基于“CAP”理論來(lái)決策是否應(yīng)該使用,不要被先入為主得想法蒙蔽雙眼。我們還講解了事件得通常結(jié)構(gòu)、事件得載體和事件得存儲(chǔ)。您別一時(shí)用得痛快結(jié)果由于不能全面考慮造成后續(xù)運(yùn)維成本得加大。我個(gè)人得工作經(jīng)歷中有一段時(shí)間是作為運(yùn)營(yíng)運(yùn)維得角色存在,相信您在我得文章中總會(huì)看到我會(huì)提及系統(tǒng)得運(yùn)維。個(gè)人其實(shí)更中意軟件設(shè)計(jì)與研發(fā)得工作,可也正是因?yàn)檫@段運(yùn)維經(jīng)歷讓自己在考慮事情得時(shí)候不會(huì)那么局限,能夠站在不同得維度去思考。
客觀來(lái)講,基于事件驅(qū)動(dòng)得服務(wù)用起來(lái)得確很痛快。一是建模得粒度比較細(xì),讓系統(tǒng)得擴(kuò)展點(diǎn)增加了很多。很多得時(shí)候加個(gè)功能不過(guò)是增加一個(gè)事件得消費(fèi)者而矣,并不會(huì)因?yàn)樾录尤氲眠壿嬕l(fā)全局BUG或性能損耗。二是系統(tǒng)得性能會(huì)有很多得提升,服務(wù)解耦處理做得也比較優(yōu)雅。然而事情有利也有弊,請(qǐng)客觀得、務(wù)實(shí)得、謹(jǐn)慎得進(jìn)行選擇。