稍微回想一下計算機硬件的工做原理咱們便不難發現,整個計算機的工做過程其實就是一個對事件的處理過程。當你點擊鼠標、敲擊鍵盤或者插上U盤時,計算機便以中斷的形式處理各類外部事件。在軟件開發領域,事件驅動架構(Event Driven Architecture,EDA)早已被開發者用於各類實踐,典型的應用場景好比瀏覽器對用戶輸入的處理、消息機制以及SOA。最近幾年從新進入開發者視野的響應式編程(Reactive Programming)更是將事件做爲該編程模型中的一等公民。可見,「事件」這個概念一直在計算機科學領域中扮演着重要的角色。數據庫
若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。編程
認識領域事件瀏覽器
領域事件(Domain Events)是領域驅動設計(Domain Driven Design,DDD)中的一個概念,用於捕獲咱們所建模的領域中所發生過的事情。領域事件自己也做爲通用語言(Ubiquitous Language)的一部分紅爲包括領域專家在內的全部項目成員的交流用語。好比,在用戶註冊過程當中,咱們可能會說「當用戶註冊成功以後,發送一封歡迎郵件給客戶。」,此時的「用戶已經註冊」即是一個領域事件。服務器
固然,並非全部發生過的事情均可以成爲領域事件。一個領域事件必須對業務有價值,有助於造成完整的業務閉環,也即一個領域事件將致使進一步的業務操做。舉個咖啡廳建模的例子,當客戶來到前臺時將產生「客戶已到達」的事件,若是你關注的是客戶接待,好比須要爲客戶預留位置等,那麼此時的「客戶已到達」即是一個典型的領域事件,由於它將用於觸發下一步——「預留位置」操做;可是若是你建模的是咖啡結帳系統,那麼此時的「客戶已到達」便沒有多大存在的必要——你不可能在用戶到達時就當即向客戶要錢對吧,而」客戶已下單「纔是對結帳系統有用的事件。架構
在微服務(Microservices)架構實踐中,人們大量地借用了DDD中的概念和技術,好比一個微服務應該對應DDD中的一個限界上下文(Bounded Context);在微服務設計中應該首先識別出DDD中的聚合根(Aggregate Root);還有在微服務之間集成時採用DDD中的防腐層(Anti-Corruption Layer, ACL);咱們甚至能夠說DDD和微服務有着天生的默契。更多有關DDD的內容,請參考筆者的另外一篇文章或參考《領域驅動設計》及《實現領域驅動設計》。併發
在DDD中有一條原則:一個業務用例對應一個事務,一個事務對應一個聚合根,也即在一次事務中,只能對一個聚合根進行操做。可是在實際應用中,咱們常常發現一個用例須要修改多個聚合根的狀況,而且不一樣的聚合根還處於不一樣的限界上下文中。好比,當你在電商網站上買了東西以後,你的積分會相應增長。這裏的購買行爲可能被建模爲一個訂單(Order)對象,而積分能夠建模成帳戶(Account)對象的某個屬性,訂單和帳戶均爲聚合根,而且分別屬於訂單系統和帳戶系統。顯然,咱們須要在訂單和積分之間維護數據一致性,然而在同一個事務中同時更新二者又違背了DDD設計原則,而且此時須要在兩個不一樣的系統之間採用重量級的分佈式事務(Distributed Transactioin,也叫XA事務或者全局事務)。另外,這種方式還在訂單系統和帳戶系統之間產生了強耦合。經過引入領域事件,咱們能夠很好地解決上述問題。框架
總的來講,領域事件給咱們帶來如下好處:dom
仍是以上面的電商網站爲例,當用戶下單以後,訂單系統將發出一個「用戶已下單」的領域事件,併發布到消息系統中,此時下單便完成了。帳戶系統訂閱了消息系統中的「用戶已下單」事件,當事件到達時進行處理,提取事件中的訂單信息,再調用自身的積分引擎(也有多是另外一個微服務)計算積分,最後更新用戶積分。能夠看到,此時的訂單系統在發送了事件以後,整個用例操做便結束了,根本不用關心是誰收到了事件或者對事件作了什麼處理。事件的消費方能夠是帳戶系統,也能夠是任何一個對事件感興趣的第三方,好比物流系統。由此,各個微服務之間的耦合關係便解開了。值得注意的一點是,此時各個微服務之間再也不是強一致性,而是基於事件的最終一致性。異步
事件風暴(Event Storming)分佈式
事件風暴是一項團隊活動,旨在經過領域事件識別出聚合根,進而劃分微服務的限界上下文。在活動中,團隊先經過頭腦風暴的形式羅列出領域中全部的領域事件,整合以後造成最終的領域事件集合,而後對於每個事件,標註出致使該事件的命令(Command),再而後爲每一個事件標註出命令發起方的角色,命令能夠是用戶發起,也能夠是第三方系統調用或者是定時器觸發等。最後對事件進行分類整理出聚合根以及限界上下文。事件風暴還有一個額外的好處是能夠加深參與人員對領域的認識。須要注意的是,在事件風暴活動中,領域專家是必須在場的。更多有關事件風暴的內容,請參考這裏。
建立領域事件
領域事件應該回答「什麼人何時作了什麼事情」這樣的問題,在實際編碼中,能夠考慮採用層超類型(Layer Supertype)來包含事件的某些共有屬性:
若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。
public abstract class Event { private final UUID id; private final DateTime createdTime; public Event() { this.id = UUID.randomUUID(); this.createdTime = new DateTime(); } }
能夠看到,領域事件還包含了ID,可是該ID並非實體(Entity)層面的ID概念,而是主要用於事件追溯和日誌。另外,因爲領域事件描述的是過去發生的事情,咱們應該將領域事件建模成不可變的(Immutable)。從DDD概念上講,領域事件更像一種特殊的值對象(Value Object)。對於上文中提到的咖啡廳例子,建立「客戶已到達」事件以下:
public final class CustomerArrivedEvent extends Event { private final int customerNumber; public CustomerArrivedEvent(int customerNumber) { super(); this.customerNumber = customerNumber; } }
在這個CustomerArrivedEvent事件中,除了繼承自Event的屬性外,還自定義了一個與該事件密切關聯的業務屬性——客戶人數(customerNumber)——這樣後續操做即可預留相應數目的座位了。另外,咱們將全部屬性以及CustomerArrivedEvent自己都聲明成了final,而且不向外暴露任何可能修改這些屬性的方法,這樣便保證了事件的不變性。
發佈領域事件
在使用領域事件時,咱們一般採用「發佈-訂閱」的方式來集成不一樣的模塊或系統。在單個微服務內部,咱們可使用領域事件來集成不一樣的功能組件,好比在上文中提到的「用戶註冊以後向用戶發送歡迎郵件」的例子中,註冊組件發出一個事件,郵件發送組件接收到該事件後向用戶發送郵件。
在微服務內部使用領域事件時,咱們不必定非得引入消息中間件(好比ActiveMQ等)。仍是以上面的「註冊後發送歡迎郵件」爲例,註冊行爲和發送郵件行爲雖然經過領域事件集成,可是他們依然發生在同一個線程中,而且是同步的。另外須要注意的是,在限界上下文以內使用領域事件時,咱們依然須要遵循「一個事務只更新一個聚合根」的原則,違反之每每意味着咱們對聚合根的拆分是錯的。即使確實存在這樣的狀況,也應該經過異步的方式(此時須要引入消息中間件)對不一樣的聚合根採用不一樣的事務,此時能夠考慮使用後臺任務。
除了用於微服務的內部,領域事件更多的是被用於集成不一樣的微服務,如上文中的「電商訂單」例子。
若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。
一般,領域事件產生於領域對象中,或者更準確的說是產生於聚合根中。在具體編碼實現時,有多種方式可用於發佈領域事件。
一種直接的方式是在聚合根中直接調用發佈事件的Service對象。以上文中的「電商訂單」爲例,當建立訂單時,發佈「訂單已建立」領域事件。此時能夠考慮在訂單對象的構造函數中發佈事件:
public class Order { public Order(EventPublisher eventPublisher) { //create order //… eventPublisher.publish(new OrderPlacedEvent()); } }
注:爲了把焦點集中在事件發佈上,咱們對Order對象作了簡化,Order對象自己在實際編碼中不具有參考性。
能夠看到,爲了發佈OrderPlacedEvent事件,咱們須要將Service對象EventPublisher傳入,這顯然是一種API污染,即Order做爲一個領域對象只須要關注和業務相關的數據,而不是諸如EventPublisher這樣的基礎設施對象。 另外一種方法是由NServiceBus的創始人Udi Dahan提出來的,即在領域對象中經過調用EventPublisher上的靜態方法發佈領域事件:
public class Order { public Order() { //create order //... EventPublisher.publish(new OrderPlacedEvent()); } }
若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。
這種方法雖然避免了API污染,可是這裏的publish()靜態方法將產生反作用,對Order對象的測試帶來了難處。此時,咱們能夠採用「在聚合根中臨時保存領域事件」的方式予以改進:
public class Order { private List<Event> events; public Order() { //create order //... events.add(new OrderPlacedEvent()); } public List<Event> getEvents() { return events; } public void clearEvents() { events.clear(); } }
在測試Order對象時,咱們便你能夠經過驗證events集合保證Order對象在建立時的確發佈了OrderPlacedEvent事件:
@Test public void shouldPublishEventWhenCreateOrder() { Order order = new Order(); List<Event> events = order.getEvents(); assertEquals(1, events.size()); Event event = events.get(0); assertTrue(event instanceof OrderPlacedEvent); }
在這種方式中,聚合根對領域事件的保存只能是臨時的,在對該聚合根操做完成以後,咱們應該將領域事件發佈出去並及時清空events集合。能夠考慮在持久化聚合根時進行這樣的操做,在DDD中即爲資源庫(Repository):
public class OrderRepository { private EventPublisher eventPublisher; public void save(Order order) { //save the order //... List<Event> events = order.getEvents(); events.forEach(event -> eventPublisher.publish(event)); order.clearEvents(); } }
除此以外,還有一種與「臨時保存領域事件」類似的作法是「在聚合根方法中直接返回領域事件」,而後在Repository中進行發佈。這種方式依然有很好的可測性,而且開發人員不用手動清空先前的事件集合,不過仍是得記住在Repository中將事件發佈出去。另外,這種方式不適合建立聚合根的場景,由於此時的建立過程既要返回聚合根自己,又要返回領域事件。
這種方式也有很差的地方,好比它要求開發人員在每次更新聚合根時都必須記得清空events集合,忘記這麼作將爲程序帶來嚴重的bug。不過雖然如此,這依然是筆者比較推薦的方式。
業務操做和事件發佈的原子性
雖然在不一樣聚合根之間咱們採用了基於領域事件的最終一致性,可是在業務操做和事件發佈之間咱們依然須要採用強一致性,也即這二者的發生應該是原子的,要麼所有成功,要麼所有失敗,不然最終一致性根本無從談起。以上文中「訂單積分」爲例,若是客戶下單成功,可是事件發送失敗,下游的帳戶系統便拿不到事件,致使最終客戶的積分並不增長。
要保證業務操做和事件發佈之間的原子性,最直接的方法即是採用XA事務,好比Java中的JTA,這種方式因爲其重量級並不被人們所看好。可是,對於一些對性能要求不那麼高的系統,這種方式何嘗不是一個選擇。一些開發框架已經可以支持獨立於應用服務器的XA事務管理器(如Atomikos 和Bitronix),好比Spring Boot做爲一個微服務框架便提供了對Atomikos和Bitronix的支持。
若是JTA不是你的選項,那麼能夠考慮採用事件表的方式。這種方式首先將事件保存到聚合根所在的數據庫中,因爲事件表和聚合根表同屬一個數據庫,整個過程只須要一個本地事務就能完成。而後,在一個單獨的後臺任務中讀取事件表中未發佈的事件,再將事件發佈到消息中間件中。
這種方式須要注意兩個問題,第一個是因爲發佈了事件以後須要將表中的事件標記成「已發佈」狀態,即依然涉及到對數據庫的操做,所以發佈事件和標記「已發佈」之間須要原子性。固然,此時依舊能夠採用XA事務,可是這違背了採用事件表的初衷。一種解決方法是將事件的消費方建立成冪等的,即消費方能夠屢次消費同一個事件。這個過程大體爲:整個過程當中事件發送和數據庫更新採用各自的事務管理,此時有可能發生的狀況是事件發送成功而數據庫更新失敗,這樣在下一次事件發佈操做中,因爲先前發佈過的事件在數據庫中依然是「未發佈」狀態,該事件將被從新發布到消息系統中,致使事件重複,但因爲事件的消費方是冪等的,所以事件重複不會存在問題。
另一個須要注意的問題是持久化機制的選擇。其實對於DDD中的聚合根來講,NoSQL是相比於關係型數據庫更合適的選擇,好比用MongoDB的Document保存聚合根即是種很天然的方式。可是多數NoSQL是不支持ACID的,也就是說不能保證聚合更新和事件發佈之間的原子性。還好,關係型數據庫也在向NoSQL方向發展,好比新版本的PostgreSQL(版本9.4)和MySQL(版本5.7)已經可以提供具有NoSQL特徵的JSON存儲和基於JSON的查詢。此時,咱們能夠考慮將聚合根序列化成JSON格式的數據進行保存,從而避免了使用重量級的ORM工具,又能夠在多個數據之間保證ACID,何樂而不爲?
若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:854630135,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。
總結
領域事件主要用於解耦微服務,此時各個微服務之間將造成最終一致性。事件風暴活動有助於咱們對微服務進行拆分,而且有助於咱們深刻了解某個領域。領域事件做爲已經發生過的歷史數據,在建模時應該將其建立爲不可變的特殊值對象。存在多種方式用於發佈領域事件,其中「在聚合中臨時保存領域事件」的方式是值得推崇的。另外,咱們須要考慮到聚合更新和事件發佈之間的原子性,能夠考慮使用XA事務或者採用單獨的事件表。爲了不事件重複帶來的問題,最好的方式是將事件的消費方建立爲冪等的。