自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

在微服務(wù)中使用領(lǐng)域事件

開發(fā) 開發(fā)工具
領(lǐng)域事件主要用于解耦微服務(wù),此時各個微服務(wù)之間將形成最終一致性。事件風暴活動有助于我們對微服務(wù)進行拆分,并且有助于我們深入了解某個領(lǐng)域。

稍微回想一下計算機硬件的工作原理我們便不難發(fā)現(xiàn),整個計算機的工作過程其實就是一個對事件的處理過程。當你點擊鼠標、敲擊鍵盤或者插上U盤時,計算機便以中斷的形式處理各種外部事件。

在軟件開發(fā)領(lǐng)域,事件驅(qū)動架構(gòu)(Event Driven Architecture,EDA)早已被開發(fā)者用于各種實踐,典型的應用場景比如瀏覽器對用戶輸入的處理、消息機制以及SOA。最近幾年重新進入開發(fā)者視野的響應式編程(Reactive Programming)更是將事件作為該編程模型中的一等公民。可見,“事件”這個概念一直在計算機科學領(lǐng)域中扮演著重要的角色。

[[188895]]

認識領(lǐng)域事件

領(lǐng)域事件(Domain Events)是領(lǐng)域驅(qū)動設(shè)計(Domain Driven Design,DDD)中的一個概念,用于捕獲我們所建模的領(lǐng)域中所發(fā)生過的事情。領(lǐng)域事件本身也作為通用語言(Ubiquitous Language)的一部分成為包括領(lǐng)域?qū)<以趦?nèi)的所有項目成員的交流用語。比如,在用戶注冊過程中,我們可能會說“當用戶注冊成功之后,發(fā)送一封歡迎郵件給客戶”,此時的“用戶已經(jīng)注冊”便是一個領(lǐng)域事件。

當然,并不是所有發(fā)生過的事情都可以成為領(lǐng)域事件。一個領(lǐng)域事件必須對業(yè)務(wù)有價值,有助于形成完整的業(yè)務(wù)閉環(huán),也即一個領(lǐng)域事件將導致進一步的業(yè)務(wù)操作。舉個咖啡廳建模的例子,當客戶來到前臺時將產(chǎn)生“客戶已到達”的事件,如果你關(guān)注的是客戶接待,比如需要為客戶預留位置等,那么此時的“客戶已到達”便是一個典型的領(lǐng)域事件,因為它將用于觸發(fā)下一步——“預留位置”操作;但是如果你建模的是咖啡結(jié)賬系統(tǒng),那么此時的“客戶已到達”便沒有多大存在的必要——你不可能在用戶到達時就立即向客戶要錢對吧,而”客戶已下單“才是對結(jié)賬系統(tǒng)有用的事件。

在微服務(wù)(Microservices)架構(gòu)實踐中,人們大量地借用了DDD中的概念和技術(shù),比如一個微服務(wù)應該對應DDD中的一個限界上下文(Bounded Context);在微服務(wù)設(shè)計中應該首先識別出DDD中的聚合根(Aggregate Root);還有在微服務(wù)之間集成時采用DDD中的防腐層(Anti-Corruption Layer, ACL);我們甚至可以說DDD和微服務(wù)有著天生的默契。更多有關(guān)DDD的內(nèi)容,請參考筆者的另一篇文章或參考《領(lǐng)域驅(qū)動設(shè)計》及《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》。

在DDD中有一條原則:一個業(yè)務(wù)用例對應一個事務(wù),一個事務(wù)對應一個聚合根,也即在一次事務(wù)中,只能對一個聚合根進行操作。

但是在實際應用中,我們經(jīng)常發(fā)現(xiàn)一個用例需要修改多個聚合根的情況,并且不同的聚合根還處于不同的限界上下文中。比如,當你在電商網(wǎng)站上買了東西之后,你的積分會相應增加。這里的購買行為可能被建模為一個訂單(Order)對象,而積分可以建模成賬戶(Account)對象的某個屬性,訂單和賬戶均為聚合根,并且分別屬于訂單系統(tǒng)和賬戶系統(tǒng)。顯然,我們需要在訂單和積分之間維護數(shù)據(jù)一致性,通常的做法是在同一個事務(wù)中同時更新兩者,但是這會存在以下問題:

  • 違背DDD中"單個事務(wù)修改單個聚合根"的設(shè)計原則;
  • 需要在不同的系統(tǒng)之間采用重量級的分布式事務(wù)(Distributed Transactioin,也叫XA事務(wù)或者全局事務(wù));
  • 在不同系統(tǒng)之間產(chǎn)生強耦合

通過引入領(lǐng)域事件,我們可以很好地解決上述問題。 總的來說,領(lǐng)域事件給我們帶來以下好處:

  • 解耦微服務(wù)(限界上下文);
  • 幫助我們深入理解領(lǐng)域模型;
  • 提供審計和報告的數(shù)據(jù)來源;
  • 邁向事件溯源(Event Sourcing)和CQRS等。

還是以上面的電商網(wǎng)站為例,當用戶下單之后,訂單系統(tǒng)將發(fā)出一個“用戶已下單”的領(lǐng)域事件,并發(fā)布到消息系統(tǒng)中,此時下單便完成了。賬戶系統(tǒng)訂閱了消息系統(tǒng)中的“用戶已下單”事件,當事件到達時進行處理,提取事件中的訂單信息,再調(diào)用自身的積分引擎(也有可能是另一個微服務(wù))計算積分,***更新用戶積分。

可以看到,此時的訂單系統(tǒng)在發(fā)送了事件之后,整個用例操作便結(jié)束了,根本不用關(guān)心是誰收到了事件或者對事件做了什么處理。事件的消費方可以是賬戶系統(tǒng),也可以是任何一個對事件感興趣的第三方,比如物流系統(tǒng)。由此,各個微服務(wù)之間的耦合關(guān)系便解開了。值得注意的一點是,此時各個微服務(wù)之間不再是強一致性,而是基于事件的最終一致性。

訂單系統(tǒng)

事件風暴(Event Storming)

事件風暴是一項團隊活動,旨在通過領(lǐng)域事件識別出聚合根,進而劃分微服務(wù)的限界上下文。在活動中,團隊先通過頭腦風暴的形式羅列出領(lǐng)域中所有的領(lǐng)域事件,整合之后形成最終的領(lǐng)域事件集合,然后對于每一個事件,標注出導致該事件的命令(Command),再然后為每個事件標注出命令發(fā)起方的角色,命令可以是用戶發(fā)起,也可以是第三方系統(tǒng)調(diào)用或者是定時器觸發(fā)等。***對事件進行分類整理出聚合根以及限界上下文。

事件風暴還有一個額外的好處是可以加深參與人員對領(lǐng)域的認識。需要注意的是,在事件風暴活動中,領(lǐng)域?qū)<沂潜仨氃趫龅?。更多有關(guān)事件風暴的內(nèi)容,請參考這里。

[[188897]]

創(chuàng)建領(lǐng)域事件

領(lǐng)域事件應該回答“什么人什么時候做了什么事情”這樣的問題,在實際編碼中,可以考慮采用層超類型(Layer Supertype)來包含事件的某些共有屬性:

  1. public abstract class Event { 
  2.   private final UUID id; 
  3.   private final DateTime createdTime; 
  4.  
  5.   public Event() { 
  6.     this.id = UUID.randomUUID(); 
  7.     this.createdTime = new DateTime(); 
  8.   } 

可以看到,領(lǐng)域事件還包含了ID,但是該ID并不是實體(Entity)層面的ID概念,而是主要用于事件追溯和日志。另外,由于領(lǐng)域事件描述的是過去發(fā)生的事情,我們應該將領(lǐng)域事件建模成不可變的(Immutable)。從DDD概念上講,領(lǐng)域事件更像一種特殊的值對象(Value Object)。對于上文中提到的咖啡廳例子,創(chuàng)建“客戶已到達”事件如下:

  1. public final class CustomerArrivedEvent extends Event { 
  2.   private final int customerNumber; 
  3.  
  4.   public CustomerArrivedEvent(int customerNumber) { 
  5.     super(); 
  6.     this.customerNumber = customerNumber; 
  7.   } 

在這個CustomerArrivedEvent事件中,除了繼承自Event的屬性外,還自定義了一個與該事件密切關(guān)聯(lián)的業(yè)務(wù)屬性——客戶人數(shù)(customerNumber)——這樣后續(xù)操作便可預留相應數(shù)目的座位了。另外,我們將所有屬性以及CustomerArrivedEvent本身都聲明成了final,并且不向外暴露任何可能修改這些屬性的方法,這樣便保證了事件的不變性。

發(fā)布領(lǐng)域事件

在使用領(lǐng)域事件時,我們通常采用“發(fā)布-訂閱”的方式來集成不同的模塊或系統(tǒng)。在單個微服務(wù)內(nèi)部,我們可以使用領(lǐng)域事件來集成不同的功能組件,比如在上文中提到的“用戶注冊之后向用戶發(fā)送歡迎郵件”的例子中,注冊組件發(fā)出一個事件,郵件發(fā)送組件接收到該事件后向用戶發(fā)送郵件。

發(fā)布領(lǐng)域事件

在微服務(wù)內(nèi)部使用領(lǐng)域事件時,我們不一定非得引入消息中間件(比如ActiveMQ等)。還是以上面的“注冊后發(fā)送歡迎郵件”為例,注冊行為和發(fā)送郵件行為雖然通過領(lǐng)域事件集成,但是他們依然發(fā)生在同一個線程中,并且是同步的。另外需要注意的是,在限界上下文之內(nèi)使用領(lǐng)域事件時,我們依然需要遵循“一個事務(wù)只更新一個聚合根”的原則,違反之往往意味著我們對聚合根的拆分是錯的。即便確實存在這樣的情況,也應該通過異步的方式(此時需要引入消息中間件)對不同的聚合根采用不同的事務(wù),此時可以考慮使用后臺任務(wù)。

除了用于微服務(wù)的內(nèi)部,領(lǐng)域事件更多的是被用于集成不同的微服務(wù),如上文中的“電商訂單”例子。

發(fā)布領(lǐng)域事件

通常,領(lǐng)域事件產(chǎn)生于領(lǐng)域?qū)ο笾?,或者更準確的說是產(chǎn)生于聚合根中。在具體編碼實現(xiàn)時,有多種方式可用于發(fā)布領(lǐng)域事件。

一種直接的方式是在聚合根中直接調(diào)用發(fā)布事件的Service對象。以上文中的“電商訂單”為例,當創(chuàng)建訂單時,發(fā)布“訂單已創(chuàng)建”領(lǐng)域事件。此時可以考慮在訂單對象的構(gòu)造函數(shù)中發(fā)布事件:

  1. public class Order { 
  2.   public Order(EventPublisher eventPublisher) { 
  3.     //create order         
  4.     //…         
  5.     eventPublisher.publish(new OrderPlacedEvent());     
  6.     } 

(注:為了把焦點集中在事件發(fā)布上,我們對Order對象做了簡化,Order對象本身在實際編碼中不具備參考性。)

可以看到,為了發(fā)布OrderPlacedEvent事件,我們需要將Service對象EventPublisher傳入,這顯然是一種API污染,即Order作為一個領(lǐng)域?qū)ο笾恍枰P(guān)注和業(yè)務(wù)相關(guān)的數(shù)據(jù),而不是諸如EventPublisher這樣的基礎(chǔ)設(shè)施對象。另一種方法是由NServiceBus的創(chuàng)始人Udi Dahan提出來的,即在領(lǐng)域?qū)ο笾型ㄟ^調(diào)用EventPublisher上的靜態(tài)方法發(fā)布領(lǐng)域事件:

  1. public class Order { 
  2.   public Order() { 
  3.     //create order 
  4.     //... 
  5.     EventPublisher.publish(new OrderPlacedEvent()); 
  6.   } 

這種方法雖然避免了API污染,但是這里的publish()靜態(tài)方法將產(chǎn)生副作用,對Order對象的測試帶來了難處。此時,我們可以采用“在聚合根中臨時保存領(lǐng)域事件”的方式予以改進:

  1. public class Order { 
  2.  
  3.   private List<Event> events; 
  4.  
  5.   public Order() { 
  6.     //create order 
  7.     //... 
  8.     events.add(new OrderPlacedEvent()); 
  9.   } 
  10.  
  11.   public List<Event> getEvents() { 
  12.     return events; 
  13.   } 
  14.  
  15.   public void clearEvents() { 
  16.     events.clear(); 
  17.  
  18.   } 

在測試Order對象時,我們便你可以通過驗證events集合保證Order對象在創(chuàng)建時的確發(fā)布了OrderPlacedEvent事件:

  1. @Test 
  2. public void shouldPublishEventWhenCreateOrder() { 
  3.   Order order = new Order(); 
  4.   List<Event> events = order.getEvents(); 
  5.   assertEquals(1, events.size()); 
  6.   Event event = events.get(0); 
  7.   assertTrue(event instanceof OrderPlacedEvent); 

在這種方式中,聚合根對領(lǐng)域事件的保存只能是臨時的,在對該聚合根操作完成之后,我們應該將領(lǐng)域事件發(fā)布出去并及時清空events集合。可以考慮在持久化聚合根時進行這樣的操作,在DDD中即為資源庫(Repository):

  1. public class OrderRepository { 
  2.   private EventPublisher eventPublisher; 
  3.  
  4.   public void save(Order order) { 
  5.     List<Event> events = order.getEvents(); 
  6.     events.forEach(event -> eventPublisher.publish(event)); 
  7.     order.clearEvents(); 
  8.          
  9.     //save the order 
  10.     //... 
  11.   } 

除此之外,還有一種與“臨時保存領(lǐng)域事件”相似的做法是“在聚合根方法中直接返回領(lǐng)域事件”,然后在Repository中進行發(fā)布。這種方式依然有很好的可測性,并且開發(fā)人員不用手動清空先前的事件集合,不過還是得記住在Repository中將事件發(fā)布出去。另外,這種方式不適合創(chuàng)建聚合根的場景,因為此時的創(chuàng)建過程既要返回聚合根本身,又要返回領(lǐng)域事件。

這種方式也有不好的地方,比如它要求開發(fā)人員在每次更新聚合根時都必須記得清空events集合,忘記這么做將為程序帶來嚴重的bug。不過雖然如此,這依然是筆者比較推薦的方式。

業(yè)務(wù)操作和事件發(fā)布的原子性

雖然在不同聚合根之間我們采用了基于領(lǐng)域事件的最終一致性,但是在業(yè)務(wù)操作和事件發(fā)布之間我們依然需要采用強一致性,也即這兩者的發(fā)生應該是原子的,要么全部成功,要么全部失敗,否則最終一致性根本無從談起。以上文中“訂單積分”為例,如果客戶下單成功,但是事件發(fā)送失敗,下游的賬戶系統(tǒng)便拿不到事件,導致最終客戶的積分并不增加。

要保證業(yè)務(wù)操作和事件發(fā)布之間的原子性,最直接的方法便是采用XA事務(wù),比如Java中的JTA,這種方式由于其重量級并不被人們所看好。但是,對于一些對性能要求不那么高的系統(tǒng),這種方式未嘗不是一個選擇。一些開發(fā)框架已經(jīng)能夠支持獨立于應用服務(wù)器的XA事務(wù)管理器(如Atomikos和Bitronix),比如Spring Boot作為一個微服務(wù)框架便提供了對Atomikos和Bitronix的支持。

如果JTA不是你的選項,那么可以考慮采用事件表的方式。這種方式首先將事件保存到聚合根所在的數(shù)據(jù)庫中,由于事件表和聚合根表同屬一個數(shù)據(jù)庫,整個過程只需要一個本地事務(wù)就能完成。然后,在一個單獨的后臺任務(wù)中讀取事件表中未發(fā)布的事件,再將事件發(fā)布到消息中間件中。

業(yè)務(wù)操作和事件發(fā)布的原子性

這種方式需要注意兩個問題,***個是由于發(fā)布了事件之后需要將表中的事件標記成“已發(fā)布”狀態(tài),即依然涉及到對數(shù)據(jù)庫的操作,因此發(fā)布事件和標記“已發(fā)布”之間需要原子性。當然,此時依舊可以采用XA事務(wù),但是這違背了采用事件表的初衷。

一種解決方法是將事件的消費方創(chuàng)建成冪等的,即消費方可以多次消費同一個事件而不污染系統(tǒng)數(shù)據(jù)。這個過程大致為:整個過程中事件發(fā)送和數(shù)據(jù)庫更新采用各自的事務(wù)管理,此時有可能發(fā)生的情況是事件發(fā)送成功而數(shù)據(jù)庫更新失敗,這樣在下一次事件發(fā)布操作中,由于先前發(fā)布過的事件在數(shù)據(jù)庫中依然是“未發(fā)布”狀態(tài),該事件將被重新發(fā)布到消息系統(tǒng)中,導致事件重復,但由于事件的消費方是冪等的,因此事件重復不會存在問題。

另外一個需要注意的問題是持久化機制的選擇。其實對于DDD中的聚合根來說,NoSQL是相比于關(guān)系型數(shù)據(jù)庫更合適的選擇,比如用MongoDB的Document保存聚合根便是種很自然的方式。但是多數(shù)NoSQL是不支持ACID的,也就是說不能保證聚合更新和事件發(fā)布之間的原子性。

還好,關(guān)系型數(shù)據(jù)庫也在向NoSQL方向發(fā)展,比如新版本的PostgreSQL(版本9.4)和MySQL(版本5.7)已經(jīng)能夠提供具備NoSQL特征的JSON存儲和基于JSON的查詢。此時,我們可以考慮將聚合根序列化成JSON格式的數(shù)據(jù)進行保存,從而避免了使用重量級的ORM工具,又可以在多個數(shù)據(jù)之間保證ACID,何樂而不為?

總結(jié)

領(lǐng)域事件主要用于解耦微服務(wù),此時各個微服務(wù)之間將形成最終一致性。事件風暴活動有助于我們對微服務(wù)進行拆分,并且有助于我們深入了解某個領(lǐng)域。領(lǐng)域事件作為已經(jīng)發(fā)生過的歷史數(shù)據(jù),在建模時應該將其創(chuàng)建為不可變的特殊值對象。存在多種方式用于發(fā)布領(lǐng)域事件,其中“在聚合中臨時保存領(lǐng)域事件”的方式是值得推崇的。另外,我們需要考慮到聚合更新和事件發(fā)布之間的原子性,可以考慮使用XA事務(wù)或者采用單獨的事件表。為了避免事件重復帶來的問題,***的方式是將事件的消費方創(chuàng)建為冪等的。

【本文是51CTO專欄作者“ThoughtWorks”的原創(chuàng)稿件,微信公眾號:思特沃克,轉(zhuǎn)載請聯(lián)系原作者】

戳這里,看該作者更多好文

責任編輯:趙寧寧 來源: 51CTO專欄
相關(guān)推薦

2017-07-10 10:51:21

微服務(wù)領(lǐng)域事件Microservic

2020-12-01 12:08:45

微服務(wù)架構(gòu)DOMA

2023-06-21 08:00:00

微服務(wù)架構(gòu)

2024-02-22 18:12:18

微服務(wù)架構(gòu)設(shè)計模式

2022-10-19 13:11:35

2023-07-28 13:55:40

便捷選項組件

2022-04-25 10:44:08

微服務(wù)架構(gòu)設(shè)計

2020-04-13 09:54:44

微服務(wù)子集存儲

2018-12-12 09:59:47

微服務(wù)架構(gòu)分布式系統(tǒng)

2017-06-07 11:57:26

混合云OpenStack容器

2009-07-04 00:50:38

2009-06-25 16:49:24

Hibernate

2023-11-17 12:04:39

GORM并發(fā)

2013-12-13 17:21:14

Lua腳本語言

2023-04-12 15:25:09

Bytrace鴻蒙

2023-04-10 07:23:24

軟件微服務(wù)網(wǎng)絡(luò)

2020-08-19 10:00:14

阿里云云原生微服務(wù)

2012-10-12 14:28:32

BYOD安全網(wǎng)絡(luò)

2023-09-15 12:30:06

微服務(wù)架構(gòu)管理

2012-02-22 14:26:17

服務(wù)器云計算
點贊
收藏

51CTO技術(shù)棧公眾號