领域事件 (Domain Event)
不用CQRS、不用事件溯源,还要实现领域事件吗?
如果已经建模出来了领域事件,建议在实现的时候,照样发布领域事件。领域事件是一个很好的扩展机制。也许现在没有对象监听领域事件,将来可能就有用,但是到那个时候再回头补发领域事件,可发成本比第一直接把事件发出来成本就要更高了。
领域事件是值对象吗?
是的。
一般领域事件有哪些字段?
- id 唯一标识事件
- 事件类型
- 发出事件的聚合类型
- 发出事件的聚合id
- 事件内容,业务场景数据
- 事件发生的时间
为什么领域事件要有一个id?
这个id主要是为了唯一区分事件用的,对于幂等消费很有用。
哪些对象里可以发布领域事件?
由于领域事件是聚合执行某个功能的结果的体现,所以在聚合对象内直接发领域事件最好。如果聚合做了某个功能,但是不发领域事件,而是依赖在聚合之外的某个代码里去发领域事件,那么这就破坏了聚合封装业务逻辑的目的。当用户使用聚合的时候,他还必须知道要在调用聚合后,根据返回值去发出不同的领域事件,等于这个逻辑泄露出去了。一旦在维护过程中,有人不小心忘了发送事件,容易导致产生bug。
对于像XX已创建这样的领域事件,由于创建聚合的时候,聚合本身还不存在,那么在工厂里去发送领域事件也是很合理的。
如果在具体实现的时候,受代码或者框架限制,不能在有状态的聚合对象内发送领域事件,那就没办法只能妥协了。
在聚合内发送领域事件,但是当时聚合还没有持久化,被监听到不是不符合领域事件语义了?
领域事件的发出,和被监听到是两回事。发出领域事件,领域事件被暂存到了某个地方,在之后某个时刻被投递给监听者。
- 事务内,立刻投递
- 这个时候聚合还没有被写入数据库,如果监听者要载入被修改的聚合使用里面的状态数据,那么要求必须载入和前面被修改的在内存中是同一个对象,这个通过编程技巧可以实现,有些框架已经实现。可以回滚事务。
- 事务内,提交前投递
- 这个时候,聚合变更已经写入数据库但只在当前事务可见,如果监听者要载入被修改的聚合,即使重新从数据库载入,也会载入状态正确的对象,不过要用编程技巧将调用监听者推迟到事务提交前。可以回滚事务。
- 事务外,提交后投递
- 这个时候,聚合变更已经持久化。监听者可以正常使用聚合。不能导致事务回滚,万一监听过程失败,需要做最终一致性。
需要持久化领域事件吗?
建议都持久化。领域事件内包含所有业务数据。聚合和读模型只包含了对于业务逻辑有用和用户要看的数据,不一定是全部业务数据。保存这些数据,可以用来
- 排查业务问题
- 给将来建新聚合或者读模型的时候回溯用
- 商业智能,数据挖掘
发布领域事件是广播还是队列?
领域事件本身是广播的语义,当一个事件发生了,所有的监听者都可以监听到。但是具体技术实现的时候,到底是采用发布订阅模式,还是队列模式,那和具体的技术相关,设置需要两者混用。
如何监听领域事件?
根据具体场景去选择。
- 最简单的就是采用监听者模式,同一个进程内,通过函数调用直接调用监听者,可以在同步、异步调用,在事务内,事务外调用
- 分布式场景下,可以共享存储,比如redis、数据库等,监听者轮训共享存储来实现
- 依赖第三方MQ中间件,比如ActiveMQ、RabbitMQ、Kafka等
如何把领域事件发布到分布式系统中?
关注最终一致性的,请采用发件箱模式。参看 实现-架构模式-发件箱 一节。一般这种情况都会使用消息中间件,请参看具体消息中间件的使用说明。
如何幂等消费领域事件?
如果领域事件已经有id了,那幂等消费变得很简单,只要消费者记录下来已经消费过的id,每次消费的时候查询一下是否消费过就可以了。一般把事件的id,类型等信息放到消息的头部,而不是消息体中,避免解析消息体才能获取到这些重要信息。
如果没有id,那么得结合业务场景选择合适的业务数据来做幂等了。比如,订单号。