DDD理論學習系列(9)-- 領域事件

DDD理論學習系列——案例及目錄數據庫


1. 引言

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.
領域事件是一個領域模型中極其重要的部分,用來表示領域中發生的事件。忽略不相關的領域活動,同時明確領域專家要跟蹤或但願被通知的事情,或與其餘模型對象中的狀態更改相關聯。微信

針對官方釋義,咱們能夠理出如下幾個要點:架構

  1. 領域事件做爲領域模型的重要部分,是領域建模的工具之一。
  2. 用來捕獲領域中已經發生的事情。
  3. 並非領域中全部發生的事情都要建模爲領域事件,要忽略無業務價值的事件。
  4. 領域事件是領域專家所關心的(須要跟蹤的、但願被通知的、會引發其餘模型對象改變狀態的)發生在領域中的一些事情。

簡而言之,領域事件是用來捕獲領域中發生的具備業務價值的一些事情。它的本質就是事件,不要將其複雜化。在DDD中,領域事件做爲通用語言的一種,是爲了清晰表述領域中產生的事件概念,幫助咱們深刻理解領域模型。併發

2. 認識領域事件

當用戶在購物車點擊結算時,生成待付款訂單,若支付成功,則更新訂單狀態爲已支付,扣減庫存,並推送撿貨通知信息到撿貨中心。app

在這個用例中,「訂單支付成功」就是一個領域事件。dom

考慮一下,在你沒有接觸領域事件或EDA(事件驅動架構)以前,你會如何實現這個用例。確定是簡單直接的方法調用,在一個事務中分別去調用狀態更新方法、扣減庫存方法、發送撿貨通知方法。這無可厚非,畢竟以前都是這樣乾的。分佈式

那這樣設計有什麼問題?微服務

  1. 試想一下,若如今要求支付成功後,須要額外發送一條付款成功通知到微信公衆號,咱們怎麼實現?想必咱們須要額外定義發送微信通知的接口並封裝參數,而後再添加對方法的調用。這種作法雖然能夠解決需求的變動,但很顯然不夠靈活耦合性強,也違反了OCP。
  2. 將多個操做放在同一個事務中,使用事務一致性能夠保證多個操做要麼所有成功要麼所有失敗。在一個事務中處理多個操做,若其中一個操做失敗,則所有失敗。可是,這在業務上是不容許的。客戶成功支付了,卻發現訂單依舊爲待付款,這會致使糾紛的。
  3. 違反了聚合的一大原則:在一個事務中,只對一個聚合進行修改。在這個用例中,很明顯咱們在一個事務中對訂單聚合和庫存聚合進行了修改。

那如何解決這些問題?咱們能夠藉助領域事件的力量。工具

  1. 解耦,能夠經過發佈訂閱模式,發佈領域事件,讓訂閱者自行訂閱;
  2. 經過領域事件來達到最終一致性,提升系統的穩定性和性能;
  3. 事件溯源;
  4. 等等。

下面咱們就來一一深刻。性能

3.建模領域事件

如何使用領域事件來解耦呢?
固然是封裝不變,應對萬變。那針對上面的用例,不變的是什麼,變的又是什麼?不變的是訂單支付成功這個事件;變化的是針對這個事件的不一樣處理手段。

而咱們要如何封裝呢?
這時咱們就要理清事件的本質,事件有因必有果,事件是由事件源和事件處理組合而成的。經過事件源咱們來辨別事件的來源,事件處理來表示事件致使的下一步操做。

3.1. 抽象事件源

事件源應該至少包含事件發生的時間和觸發事件的對象。咱們提取IEventData接口來封裝事件源:

/// <summary>
/// 定義事件源接口,全部的事件源都要實現該接口
/// </summary>
public interface IEventData
{
    /// <summary>
    /// 事件發生的時間
    /// </summary>
    DateTime EventTime { get; set; }

    /// <summary>
    /// 觸發事件的對象
    /// </summary>
    object EventSource { get; set; }
}

經過實現IEventData咱們能夠根據本身的須要添加自定義的事件屬性。

3.2. 抽象事件處理

針對事件處理,咱們提取一個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>來表達領域事件的概念。

3.3. 領域事件的發佈和訂閱

領域事件不是平白無故產生的,它有一個發佈方。同理,它也要有一個訂閱方。

那如何和訂閱和發佈領域事件呢?
領域事件的發佈可使用發佈--訂閱模式來實現。而比較常見的實現方式就是事件總線

事件總線是一種集中式事件處理機制,容許不一樣的組件之間進行彼此通訊而又不須要相互依賴,達到一種解耦的目的。Event Bus就至關於一個介於Publisher(發佈方)和Subscriber(訂閱方)中間的橋樑。它隔離了Publlisher和Subscriber之間的直接依賴,接管了全部事件的發佈和訂閱邏輯,並負責事件的中轉。

這裏就簡要說明一下事件總線的實現的要點:

  1. 事件總線維護一個事件源與事件處理的映射字典;
  2. 經過單例模式,確保事件總線的惟一入口;
  3. 利用反射或依賴注入完成事件源與事件處理的初始化綁定;
  4. 提供統一的事件註冊、取消註冊和觸發接口。

最後,咱們看下事件總線的接口定義:

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方法來完成領域事件的發佈。

而關於事件總線的具體實現,可參考個人這篇博文——事件總線知多少

4. 最終一致性

說到一致性,咱們要先搞明白下面幾個概念。

事務一致性
事務一致性是是數據庫事務的四個特性之一,也就是ACID特性之一:

原子性(Atomicity):事務做爲一個總體被執行,包含在其中的對數據庫的操做要麼所有被執行,要麼都不執行。
一致性(Consistency):事務應確保數據庫的狀態從一個一致狀態轉變爲另外一個一致狀態。
隔離性(Isolation):多個事務併發執行時,一個事務的執行不該影響其餘事務的執行。
持久性(Durability):已被提交的事務對數據庫的修改應該永久保存在數據庫中。

咱們用一張圖來理解一下:

事務一致性
在事務一致性的保證下,上面的圖示只會有兩個結果:

  1. A和B兩個操做都成功了。
  2. A和B兩個操做都失敗了。

數據一致性
舉個簡單的例子,假設10我的,每人有100個虛擬幣,虛擬幣僅能在這10人內流通,無論怎麼流通,最終的虛擬幣總數都是1000個,這就是數據一致性。

領域一致性
簡單理解就是在領域中的操做要知足領域中定義的業務規則。好比你轉帳,並非你餘額充足就能夠轉帳的,還要求帳戶的狀態爲非掛失、鎖定狀態。

回到咱們的案例,當支付成功後,更新訂單狀態,扣減庫存,併發送撿貨通知。按照咱們以往的作法,爲了維護訂單和庫存的數據一致性,咱們將這三個操做放到一個應用服務去作(由於應用服務管理事務),事務的一致性能夠保證要麼所有成功要麼所有失敗。可是,試想一下,客戶支付成功後,訂單依舊爲待付款狀態,這會引發糾紛。另外,因爲庫存沒有及時扣減,極可能會致使庫存超賣。怎麼辦呢?
將事務拆解,使用領域事件來達到最終一致性。

最終一致性
「最終一致性」是一種設計方法,能夠經過將某些操做的執行延遲到稍後的時間來提升應用程序的可擴展性和性能。

最終一致性

對於常見於分佈式系統的最終一致性工做流中,客戶一樣在系統中執行一個命令,但這個系統只爲維護事務中的領域一致性運行部分的操做,剩餘的操做在容許延後執行。針對上圖的結果:

  1. A操做執行成功,B操做將延後執行。
  2. A操做失敗,B操做將不會執行。

而針對咱們的案例,咱們如何使用領域事件來進行事務拆分呢?咱們看下下面這張圖你就明白了。

領域事件在最終一致性的位置

分析一下,針對咱們案例,咱們發現一個用例須要修改多個聚合根的狀況,而且不一樣的聚合根還處於不一樣的限界上下文中。其中訂單和庫存均爲聚合根,分別屬於訂單系統和庫存系統。咱們能夠這樣作:

  1. 在訂單所在的聚合根中更新訂單支付狀態,併發布「訂單成功支付」的領域事件;
  2. 而後庫存系統訂閱並處理庫存扣減邏輯;
  3. 通知系統訂閱並處理撿貨通知。

經過這種方式,咱們即保證了聚合的原則,又保證了數據的最終一致性。

5. 事件存儲和事件溯源

關於事件存儲(Event Store)和事件溯源(Event Sourcing)是一個比較複雜的概念,咱們這裏就簡單介紹下,不作過多展開,後續再設章節詳述。

事件存儲,顧名思義,即事件的持久化。那爲何要持久化事件?

  1. 當事件發佈失敗時,可用於從新發布。
  2. 經過消息中間件去分發事件,提升系統的吞吐量。
  3. 用於事件溯源。

源代碼管理工具咱們都用過,如Git、TFS、SVN等,經過記錄文件每一次的修改記錄,以便咱們跟蹤每一次對源代碼的修改,從而咱們能夠隨時回滾到文件的指定修改版本。

事件溯源的本質亦是如此,不過它存儲的並不是聚合每次變化的結果,而是存儲應用在該聚合上的歷史領域事件。當須要恢復某個狀態時,須要把應用在聚合的領域事件按序「重放」到要恢復狀態對應的領域事件爲止。

6.總結

通過上面的分析,咱們知道引入領域事件的目的主要有兩個,一是解耦,二是使用領域事件進行事務的拆分,經過引入事件存儲,來實現數據的最終一致性。

最後,對於領域事件,咱們能夠這樣理解:
經過將領域中所發生的活動建模成一系列的離散事件,並將每一個事件都用領域對象來表示,來跟蹤領域中發生的事情。
也能夠簡要理解爲:領域事件 = 事件發佈 + 事件存儲 + 事件分發 + 事件處理

以上,僅是我的理解,DDD水很深,剪不斷,理還亂,有問題或看法,歡迎指正交流。

參考資料:
在微服務中使用領域事件
使用聚合、事件溯源和CQRS開發事務型微服務
如何理解數據庫事務中的一致性的概念?
Eventual Consistency via Domain Events and Azure Service Bus

相關文章
相關標籤/搜索