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.
領(lǐng)域事件是一個(gè)領(lǐng)域模型中極其重要的部分,用來表示領(lǐng)域中發(fā)生的事件。忽略不相關(guān)的領(lǐng)域活動,同時(shí)明確領(lǐng)域?qū)<乙櫥蛳M煌ㄖ氖虑?,或與其他模型對象中的狀態(tài)更改相關(guān)聯(lián)。
針對官方釋義,我們可以理出以下幾個(gè)要點(diǎn):
- 領(lǐng)域事件作為領(lǐng)域模型的重要部分,是領(lǐng)域建模的工具之一。
- 用來捕獲領(lǐng)域中已經(jīng)發(fā)生的事情。
- 并不是領(lǐng)域中所有發(fā)生的事情都要建模為領(lǐng)域事件,要忽略無業(yè)務(wù)價(jià)值的事件。
- 領(lǐng)域事件是領(lǐng)域?qū)<宜P(guān)心的(需要跟蹤的、希望被通知的、會引起其他模型對象改變狀態(tài)的)發(fā)生在領(lǐng)域中的一些事情。
簡而言之,領(lǐng)域事件是用來捕獲領(lǐng)域中發(fā)生的具有業(yè)務(wù)價(jià)值的一些事情。它的本質(zhì)就是事件,不要將其復(fù)雜化。在DDD中,領(lǐng)域事件作為通用語言的一種,是為了清晰表述領(lǐng)域中產(chǎn)生的事件概念,幫助我們深入理解領(lǐng)域模型。
2. 認(rèn)識領(lǐng)域事件
當(dāng)用戶在購物車點(diǎn)擊結(jié)算時(shí),生成待付款訂單,若支付成功,則更新訂單狀態(tài)為已支付,扣減庫存,并推送撿貨通知信息到撿貨中心。
在這個(gè)用例中,“訂單支付成功”就是一個(gè)領(lǐng)域事件。
考慮一下,在你沒有接觸領(lǐng)域事件或EDA(事件驅(qū)動架構(gòu))之前,你會如何實(shí)現(xiàn)這個(gè)用例。肯定是簡單直接的方法調(diào)用,在一個(gè)事務(wù)中分別去調(diào)用狀態(tài)更新方法、扣減庫存方法、發(fā)送撿貨通知方法。這無可厚非,畢竟之前都是這樣干的。
那這樣設(shè)計(jì)有什么問題?
- 試想一下,若現(xiàn)在要求支付成功后,需要額外發(fā)送一條付款成功通知到微信公眾號,我們怎么實(shí)現(xiàn)?想必我們需要額外定義發(fā)送微信通知的接口并封裝參數(shù),然后再添加對方法的調(diào)用。這種做法雖然可以解決需求的變更,但很顯然不夠靈活耦合性強(qiáng),也違反了OCP。
- 將多個(gè)操作放在同一個(gè)事務(wù)中,使用事務(wù)一致性可以保證多個(gè)操作要么全部成功要么全部失敗。在一個(gè)事務(wù)中處理多個(gè)操作,若其中一個(gè)操作失敗,則全部失敗。但是,這在業(yè)務(wù)上是不允許的。客戶成功支付了,卻發(fā)現(xiàn)訂單依舊為待付款,這會導(dǎo)致糾紛的。
- 違反了聚合的一大原則:在一個(gè)事務(wù)中,只對一個(gè)聚合進(jìn)行修改。在這個(gè)用例中,很明顯我們在一個(gè)事務(wù)中對訂單聚合和庫存聚合進(jìn)行了修改。
那如何解決這些問題?我們可以借助領(lǐng)域事件的力量。
- 解耦,可以通過發(fā)布訂閱模式,發(fā)布領(lǐng)域事件,讓訂閱者自行訂閱;
- 通過領(lǐng)域事件來達(dá)到最終一致性,提高系統(tǒng)的穩(wěn)定性和性能;
- 事件溯源;
- 等等。
下面我們就來一一深入。
3.建模領(lǐng)域事件
如何使用領(lǐng)域事件來解耦呢?
當(dāng)然是封裝不變,應(yīng)對萬變。那針對上面的用例,不變的是什么,變的又是什么?不變的是訂單支付成功這個(gè)事件;變化的是針對這個(gè)事件的不同處理手段。
而我們要如何封裝呢?
這時(shí)我們就要理清事件的本質(zhì),事件有因必有果,事件是由事件源和事件處理組合而成的。通過事件源我們來辨別事件的來源,事件處理來表示事件導(dǎo)致的下一步操作。

3.1. 抽象事件源
事件源應(yīng)該至少包含事件發(fā)生的時(shí)間和觸發(fā)事件的對象。我們提取IEventData接口來封裝事件源:
/// <summary>
/// 定義事件源接口,所有的事件源都要實(shí)現(xiàn)該接口
/// </summary>
public interface IEventData
{
/// <summary>
/// 事件發(fā)生的時(shí)間
/// </summary>
DateTime EventTime { get; set; }
/// <summary>
/// 觸發(fā)事件的對象
/// </summary>
object EventSource { get; set; }
}
通過實(shí)現(xiàn)IEventData我們可以根據(jù)自己的需要添加自定義的事件屬性。
3.2. 抽象事件處理
針對事件處理,我們提取一個(gè)IEventHandler接口:
/// <summary>
/// 定義事件處理器公共接口,所有的事件處理都要實(shí)現(xiàn)該接口
/// </summary>
public interface IEventHandler
{
}
事件處理要與事件源進(jìn)行綁定,所以我們再來定義一個(gè)泛型接口:
/// <summary>
/// 泛型事件處理器接口
/// </summary>
/// <typeparam name="TEventData"></typeparam>
public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
{
/// <summary>
/// 事件處理器實(shí)現(xiàn)該方法來處理事件
/// </summary>
/// <param name="eventData"></param>
void HandleEvent(TEventData eventData);
}
以上,我們就完成了領(lǐng)域事件的抽象。在代碼中我們通過實(shí)現(xiàn)一個(gè)IEventHandler<T>來表達(dá)領(lǐng)域事件的概念。
3.3. 領(lǐng)域事件的發(fā)布和訂閱
領(lǐng)域事件不是無緣無故產(chǎn)生的,它有一個(gè)發(fā)布方。同理,它也要有一個(gè)訂閱方。
那如何和訂閱和發(fā)布領(lǐng)域事件呢?
領(lǐng)域事件的發(fā)布可以使用發(fā)布--訂閱模式來實(shí)現(xiàn)。而比較常見的實(shí)現(xiàn)方式就是事件總線。

事件總線是一種集中式事件處理機(jī)制,允許不同的組件之間進(jìn)行彼此通信而又不需要相互依賴,達(dá)到一種解耦的目的。Event Bus就相當(dāng)于一個(gè)介于Publisher(發(fā)布方)和Subscriber(訂閱方)中間的橋梁。它隔離了Publlisher和Subscriber之間的直接依賴,接管了所有事件的發(fā)布和訂閱邏輯,并負(fù)責(zé)事件的中轉(zhuǎn)。
這里就簡要說明一下事件總線的實(shí)現(xiàn)的要點(diǎn):
- 事件總線維護(hù)一個(gè)事件源與事件處理的映射字典;
- 通過單例模式,確保事件總線的唯一入口;
- 利用反射或依賴注入完成事件源與事件處理的初始化綁定;
- 提供統(tǒng)一的事件注冊、取消注冊和觸發(fā)接口。
最后,我們看下事件總線的接口定義:
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;
}
在應(yīng)用服務(wù)和領(lǐng)域服務(wù)中,我們都可以直接調(diào)用Register方法來完成領(lǐng)域事件的注冊,調(diào)用Trigger方法來完成領(lǐng)域事件的發(fā)布。
而關(guān)于事件總線的具體實(shí)現(xiàn),可參考我的這篇博文——事件總線知多少。
4. 最終一致性
說到一致性,我們要先搞明白下面幾個(gè)概念。
事務(wù)一致性
事務(wù)一致性是是數(shù)據(jù)庫事務(wù)的四個(gè)特性之一,也就是ACID特性之一:
原子性(Atomicity):事務(wù)作為一個(gè)整體被執(zhí)行,包含在其中的對數(shù)據(jù)庫的操作要么全部被執(zhí)行,要么都不執(zhí)行。
一致性(Consistency):事務(wù)應(yīng)確保數(shù)據(jù)庫的狀態(tài)從一個(gè)一致狀態(tài)轉(zhuǎn)變?yōu)榱硪粋€(gè)一致狀態(tài)。
隔離性(Isolation):多個(gè)事務(wù)并發(fā)執(zhí)行時(shí),一個(gè)事務(wù)的執(zhí)行不應(yīng)影響其他事務(wù)的執(zhí)行。
持久性(Durability):已被提交的事務(wù)對數(shù)據(jù)庫的修改應(yīng)該永久保存在數(shù)據(jù)庫中。
我們用一張圖來理解一下:

在事務(wù)一致性的保證下,上面的圖示只會有兩個(gè)結(jié)果:
- A和B兩個(gè)操作都成功了。
- A和B兩個(gè)操作都失敗了。
數(shù)據(jù)一致性
舉個(gè)簡單的例子,假設(shè)10個(gè)人,每人有100個(gè)虛擬幣,虛擬幣僅能在這10人內(nèi)流通,不管怎么流通,最終的虛擬幣總數(shù)都是1000個(gè),這就是數(shù)據(jù)一致性。
領(lǐng)域一致性
簡單理解就是在領(lǐng)域中的操作要滿足領(lǐng)域中定義的業(yè)務(wù)規(guī)則。比如你轉(zhuǎn)賬,并不是你余額充足就可以轉(zhuǎn)賬的,還要求賬戶的狀態(tài)為非掛失、鎖定狀態(tài)。
回到我們的案例,當(dāng)支付成功后,更新訂單狀態(tài),扣減庫存,并發(fā)送撿貨通知。按照我們以往的做法,為了維護(hù)訂單和庫存的數(shù)據(jù)一致性,我們將這三個(gè)操作放到一個(gè)應(yīng)用服務(wù)去做(因?yàn)閼?yīng)用服務(wù)管理事務(wù)),事務(wù)的一致性可以保證要么全部成功要么全部失敗。但是,復(fù)雜業(yè)務(wù)嵌套的多個(gè)操作放在一個(gè)事務(wù)中,很容易造成事務(wù)超時(shí),而往往為了性能考慮,可能會放棄事務(wù)嵌套,這樣就又很可能會導(dǎo)致:客戶支付成功后,訂單依舊為待付款狀態(tài),這會引起糾紛。另外,由于庫存沒有及時(shí)扣減,很可能會導(dǎo)致庫存超賣。怎么辦呢?
將事務(wù)拆解,使用領(lǐng)域事件來達(dá)到最終一致性。
最終一致性
“最終一致性”是一種設(shè)計(jì)方法,可以通過將某些操作的執(zhí)行延遲到稍后的時(shí)間來提高應(yīng)用程序的可擴(kuò)展性和性能。

對于常見于分布式系統(tǒng)的最終一致性工作流中,客戶同樣在系統(tǒng)中執(zhí)行一個(gè)命令,但這個(gè)系統(tǒng)只為維護(hù)事務(wù)中的領(lǐng)域一致性運(yùn)行部分的操作,剩余的操作在允許延后執(zhí)行。針對上圖的結(jié)果:
- A操作執(zhí)行成功,B操作將延后執(zhí)行。
- A操作失敗,B操作將不會執(zhí)行。
而針對我們的案例,我們?nèi)绾问褂妙I(lǐng)域事件來進(jìn)行事務(wù)拆分呢?我們看下下面這張圖你就明白了。

分析一下,針對我們案例,我們發(fā)現(xiàn)一個(gè)用例需要修改多個(gè)聚合根的情況,并且不同的聚合根還處于不同的限界上下文中。其中訂單和庫存均為聚合根,分別屬于訂單系統(tǒng)和庫存系統(tǒng)。我們可以這樣做:
- 在訂單所在的聚合根中更新訂單支付狀態(tài),并發(fā)布“訂單成功支付”的領(lǐng)域事件;
- 然后庫存系統(tǒng)訂閱并處理庫存扣減邏輯;
- 通知系統(tǒng)訂閱并處理撿貨通知。
通過這種方式,我們即保證了聚合的原則,又保證了數(shù)據(jù)的最終一致性。
5. 事件存儲和事件溯源
關(guān)于事件存儲(Event Store)和事件溯源(Event Sourcing)是一個(gè)比較復(fù)雜的概念,我們這里就簡單介紹下,不做過多展開,后續(xù)再設(shè)章節(jié)詳述。

事件存儲,顧名思義,即事件的持久化。那為什么要持久化事件?
- 當(dāng)事件發(fā)布失敗時(shí),可用于重新發(fā)布。
- 通過消息中間件去分發(fā)事件,提高系統(tǒng)的吞吐量。
- 用于事件溯源。
源代碼管理工具我們都用過,如Git、TFS、SVN等,通過記錄文件每一次的修改記錄,以便我們跟蹤每一次對源代碼的修改,從而我們可以隨時(shí)回滾到文件的指定修改版本。
事件溯源的本質(zhì)亦是如此,不過它存儲的并非聚合每次變化的結(jié)果,而是存儲應(yīng)用在該聚合上的歷史領(lǐng)域事件。當(dāng)需要恢復(fù)某個(gè)狀態(tài)時(shí),需要把應(yīng)用在聚合的領(lǐng)域事件按序“重放”到要恢復(fù)狀態(tài)對應(yīng)的領(lǐng)域事件為止。
6.總結(jié)
經(jīng)過上面的分析,我們知道引入領(lǐng)域事件的目的主要有兩個(gè),一是解耦,二是使用領(lǐng)域事件進(jìn)行事務(wù)的拆分,通過引入事件存儲,來實(shí)現(xiàn)數(shù)據(jù)的最終一致性。
最后,對于領(lǐng)域事件,我們可以這樣理解:
通過將領(lǐng)域中所發(fā)生的活動建模成一系列的離散事件,并將每個(gè)事件都用領(lǐng)域?qū)ο髞肀硎?,來跟蹤領(lǐng)域中發(fā)生的事情。
也可以簡要理解為:領(lǐng)域事件 = 事件發(fā)布 + 事件存儲 + 事件分發(fā) + 事件處理。
以上,僅是個(gè)人理解,DDD水很深,剪不斷,理還亂,有問題或見解,歡迎指正交流。
參考資料:
在微服務(wù)中使用領(lǐng)域事件
使用聚合、事件溯源和CQRS開發(fā)事務(wù)型微服務(wù)
如何理解數(shù)據(jù)庫事務(wù)中的一致性的概念?
Eventual Consistency via Domain Events and Azure Service Bus