A domain event is a full-fledged part of the domain model, a representation of something that happened in the domain. Ignore irrelevant domain activity while making explicit the events that the domain experts want to track or be notified of, or which are associated with state change in the other model objects.
領域事件是一個領域模型中極其重要的部分,用來表示領域中發生的事件。忽略不相關的領域活動,同時明確領域專家要跟蹤或但願被通知的事情,或與其餘模型對象中的狀態更改相關聯。微信
針對官方釋義,咱們能夠理出如下幾個要點:架構
簡而言之,領域事件是用來捕獲領域中發生的具備業務價值的一些事情。它的本質就是事件,不要將其複雜化。在DDD中,領域事件做爲通用語言的一種,是爲了清晰表述領域中產生的事件概念,幫助咱們深刻理解領域模型。併發
當用戶在購物車點擊結算時,生成待付款訂單,若支付成功,則更新訂單狀態爲已支付,扣減庫存,並推送撿貨通知信息到撿貨中心。app
在這個用例中,「訂單支付成功」就是一個領域事件。dom
考慮一下,在你沒有接觸領域事件或EDA(事件驅動架構)以前,你會如何實現這個用例。確定是簡單直接的方法調用,在一個事務中分別去調用狀態更新方法、扣減庫存方法、發送撿貨通知方法。這無可厚非,畢竟以前都是這樣乾的。分佈式
那這樣設計有什麼問題?微服務
那如何解決這些問題?咱們能夠藉助領域事件的力量。工具
下面咱們就來一一深刻。性能
如何使用領域事件來解耦呢?
固然是封裝不變,應對萬變。那針對上面的用例,不變的是什麼,變的又是什麼?不變的是訂單支付成功這個事件;變化的是針對這個事件的不一樣處理手段。
而咱們要如何封裝呢?
這時咱們就要理清事件的本質,事件有因必有果,事件是由事件源和事件處理組合而成的。經過事件源咱們來辨別事件的來源,事件處理來表示事件致使的下一步操做。
事件源應該至少包含事件發生的時間和觸發事件的對象。咱們提取IEventData
接口來封裝事件源:
/// <summary> /// 定義事件源接口,全部的事件源都要實現該接口 /// </summary> public interface IEventData { /// <summary> /// 事件發生的時間 /// </summary> DateTime EventTime { get; set; } /// <summary> /// 觸發事件的對象 /// </summary> object EventSource { get; set; } }
經過實現IEventData
咱們能夠根據本身的須要添加自定義的事件屬性。
針對事件處理,咱們提取一個IEventHandler
接口:
/// <summary> /// 定義事件處理器公共接口,全部的事件處理都要實現該接口 /// </summary> public interface IEventHandler { }
事件處理要與事件源進行綁定,因此咱們再來定義一個泛型接口:
/// <summary> /// 泛型事件處理器接口 /// </summary> /// <typeparam name="TEventData"></typeparam> public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData { /// <summary> /// 事件處理器實現該方法來處理事件 /// </summary> /// <param name="eventData"></param> void HandleEvent(TEventData eventData); }
以上,咱們就完成了領域事件的抽象。在代碼中咱們經過實現一個IEventHandler<T>
來表達領域事件的概念。
領域事件不是平白無故產生的,它有一個發佈方。同理,它也要有一個訂閱方。
那如何和訂閱和發佈領域事件呢?
領域事件的發佈可使用發佈--訂閱模式來實現。而比較常見的實現方式就是事件總線。
事件總線是一種集中式事件處理機制,容許不一樣的組件之間進行彼此通訊而又不須要相互依賴,達到一種解耦的目的。Event Bus就至關於一個介於Publisher(發佈方)和Subscriber(訂閱方)中間的橋樑。它隔離了Publlisher和Subscriber之間的直接依賴,接管了全部事件的發佈和訂閱邏輯,並負責事件的中轉。
這裏就簡要說明一下事件總線的實現的要點:
最後,咱們看下事件總線的接口定義:
public interface IEventBus { void Register < TEventData > (IEventHandler eventHandler); void UnRegister < TEventData > (Type handlerType) where TEventData: IEventData; void Trigger < TEventData > (Type eventHandlerType, TEventData eventData) where TEventData: IEventData; }
在應用服務和領域服務中,咱們均可以直接調用Register
方法來完成領域事件的註冊,調用Trigger
方法來完成領域事件的發佈。
而關於事件總線的具體實現,可參考個人這篇博文——事件總線知多少。
說到一致性,咱們要先搞明白下面幾個概念。
事務一致性
事務一致性是是數據庫事務的四個特性之一,也就是ACID特性之一:
原子性(Atomicity):事務做爲一個總體被執行,包含在其中的對數據庫的操做要麼所有被執行,要麼都不執行。
一致性(Consistency):事務應確保數據庫的狀態從一個一致狀態轉變爲另外一個一致狀態。
隔離性(Isolation):多個事務併發執行時,一個事務的執行不該影響其餘事務的執行。
持久性(Durability):已被提交的事務對數據庫的修改應該永久保存在數據庫中。
咱們用一張圖來理解一下:
在事務一致性的保證下,上面的圖示只會有兩個結果:
數據一致性
舉個簡單的例子,假設10我的,每人有100個虛擬幣,虛擬幣僅能在這10人內流通,無論怎麼流通,最終的虛擬幣總數都是1000個,這就是數據一致性。
領域一致性
簡單理解就是在領域中的操做要知足領域中定義的業務規則。好比你轉帳,並非你餘額充足就能夠轉帳的,還要求帳戶的狀態爲非掛失、鎖定狀態。
回到咱們的案例,當支付成功後,更新訂單狀態,扣減庫存,併發送撿貨通知。按照咱們以往的作法,爲了維護訂單和庫存的數據一致性,咱們將這三個操做放到一個應用服務去作(由於應用服務管理事務),事務的一致性能夠保證要麼所有成功要麼所有失敗。可是,試想一下,客戶支付成功後,訂單依舊爲待付款狀態,這會引發糾紛。另外,因爲庫存沒有及時扣減,極可能會致使庫存超賣。怎麼辦呢?
將事務拆解,使用領域事件來達到最終一致性。
最終一致性
「最終一致性」是一種設計方法,能夠經過將某些操做的執行延遲到稍後的時間來提升應用程序的可擴展性和性能。
對於常見於分佈式系統的最終一致性工做流中,客戶一樣在系統中執行一個命令,但這個系統只爲維護事務中的領域一致性運行部分的操做,剩餘的操做在容許延後執行。針對上圖的結果:
而針對咱們的案例,咱們如何使用領域事件來進行事務拆分呢?咱們看下下面這張圖你就明白了。
分析一下,針對咱們案例,咱們發現一個用例須要修改多個聚合根的狀況,而且不一樣的聚合根還處於不一樣的限界上下文中。其中訂單和庫存均爲聚合根,分別屬於訂單系統和庫存系統。咱們能夠這樣作:
經過這種方式,咱們即保證了聚合的原則,又保證了數據的最終一致性。
關於事件存儲(Event Store)和事件溯源(Event Sourcing)是一個比較複雜的概念,咱們這裏就簡單介紹下,不作過多展開,後續再設章節詳述。
事件存儲,顧名思義,即事件的持久化。那爲何要持久化事件?
源代碼管理工具咱們都用過,如Git、TFS、SVN等,經過記錄文件每一次的修改記錄,以便咱們跟蹤每一次對源代碼的修改,從而咱們能夠隨時回滾到文件的指定修改版本。
事件溯源的本質亦是如此,不過它存儲的並不是聚合每次變化的結果,而是存儲應用在該聚合上的歷史領域事件。當須要恢復某個狀態時,須要把應用在聚合的領域事件按序「重放」到要恢復狀態對應的領域事件爲止。
通過上面的分析,咱們知道引入領域事件的目的主要有兩個,一是解耦,二是使用領域事件進行事務的拆分,經過引入事件存儲,來實現數據的最終一致性。
最後,對於領域事件,咱們能夠這樣理解:
經過將領域中所發生的活動建模成一系列的離散事件,並將每一個事件都用領域對象來表示,來跟蹤領域中發生的事情。
也能夠簡要理解爲:領域事件 = 事件發佈 + 事件存儲 + 事件分發 + 事件處理。
以上,僅是我的理解,DDD水很深,剪不斷,理還亂,有問題或看法,歡迎指正交流。
參考資料:
在微服務中使用領域事件
使用聚合、事件溯源和CQRS開發事務型微服務
如何理解數據庫事務中的一致性的概念?
Eventual Consistency via Domain Events and Azure Service Bus