Skip to content

领域事件 (Domain Event)

不用CQRS、不用事件溯源,还要实现领域事件吗?

如果已经建模出来了领域事件,建议在实现的时候,照样发布领域事件。领域事件是一个很好的扩展机制。也许现在没有对象监听领域事件,将来可能就有用,但是到那个时候再回头补发领域事件,可发成本比第一直接把事件发出来成本就要更高了。

领域事件是值对象吗?

是的。

一般领域事件有哪些字段?

  • id 唯一标识事件
  • 事件类型
  • 发出事件的聚合类型
  • 发出事件的聚合id
  • 事件内容,业务场景数据
  • 事件发生的时间

为什么领域事件要有一个id?

这个id主要是为了唯一区分事件用的,对于幂等消费很有用。

哪些对象里可以发布领域事件?

由于领域事件是聚合执行某个功能的结果的体现,所以在聚合对象内直接发领域事件最好。如果聚合做了某个功能,但是不发领域事件,而是依赖在聚合之外的某个代码里去发领域事件,那么这就破坏了聚合封装业务逻辑的目的。当用户使用聚合的时候,他还必须知道要在调用聚合后,根据返回值去发出不同的领域事件,等于这个逻辑泄露出去了。一旦在维护过程中,有人不小心忘了发送事件,容易导致产生bug。

对于像XX已创建这样的领域事件,由于创建聚合的时候,聚合本身还不存在,那么在工厂里去发送领域事件也是很合理的。

如果在具体实现的时候,受代码或者框架限制,不能在有状态的聚合对象内发送领域事件,那就没办法只能妥协了。

在聚合内发送领域事件,但是当时聚合还没有持久化,被监听到不是不符合领域事件语义了?

领域事件的发出,和被监听到是两回事。发出领域事件,领域事件被暂存到了某个地方,在之后某个时刻被投递给监听者。

  • 事务内,立刻投递
    • 这个时候聚合还没有被写入数据库,如果监听者要载入被修改的聚合使用里面的状态数据,那么要求必须载入和前面被修改的在内存中是同一个对象,这个通过编程技巧可以实现,有些框架已经实现。可以回滚事务。
  • 事务内,提交前投递
    • 这个时候,聚合变更已经写入数据库但只在当前事务可见,如果监听者要载入被修改的聚合,即使重新从数据库载入,也会载入状态正确的对象,不过要用编程技巧将调用监听者推迟到事务提交前。可以回滚事务。
  • 事务外,提交后投递
    • 这个时候,聚合变更已经持久化。监听者可以正常使用聚合。不能导致事务回滚,万一监听过程失败,需要做最终一致性。

需要持久化领域事件吗?

建议都持久化。领域事件内包含所有业务数据。聚合和读模型只包含了对于业务逻辑有用和用户要看的数据,不一定是全部业务数据。保存这些数据,可以用来

  • 排查业务问题
  • 给将来建新聚合或者读模型的时候回溯用
  • 商业智能,数据挖掘

发布领域事件是广播还是队列?

领域事件本身是广播的语义,当一个事件发生了,所有的监听者都可以监听到。但是具体技术实现的时候,到底是采用发布订阅模式,还是队列模式,那和具体的技术相关,设置需要两者混用。

如何监听领域事件?

根据具体场景去选择。

  • 最简单的就是采用监听者模式,同一个进程内,通过函数调用直接调用监听者,可以在同步、异步调用,在事务内,事务外调用
  • 分布式场景下,可以共享存储,比如redis、数据库等,监听者轮训共享存储来实现
  • 依赖第三方MQ中间件,比如ActiveMQ、RabbitMQ、Kafka等

如何把领域事件发布到分布式系统中?

关注最终一致性的,请采用发件箱模式。参看 实现-架构模式-发件箱 一节。一般这种情况都会使用消息中间件,请参看具体消息中间件的使用说明。

如何幂等消费领域事件?

如果领域事件已经有id了,那幂等消费变得很简单,只要消费者记录下来已经消费过的id,每次消费的时候查询一下是否消费过就可以了。一般把事件的id,类型等信息放到消息的头部,而不是消息体中,避免解析消息体才能获取到这些重要信息。

如果没有id,那么得结合业务场景选择合适的业务数据来做幂等了。比如,订单号。