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

相关文章
相关标签/搜索