领域事件建模
领域事件的继承链如下:
事件基类 DomainEvent
:
public abstract class DomainEvent {
private final String _id;
private final DomainEventType _type;
private final Instant _createAt;
}
聚合根对象事件基类,比如对于订单(Order),OrderEvent
public abstract class OrderEvent extends DomainEvent {
private final String orderId;
}
然后对于实际的Order事件,统一继承自OrderEvent,比如对于创建订单的OrderCreatedEvent
事件:
public class OrderCreatedEvent extends OrderEvent {
private final BigDecimal price;
private final Address address;
private final List<OrderItem> items;
private final Instant createAt;
}
在创建领域事件时,需要注意2点:
- 领域事件本身应该是不变的(Immutable);
- 领域事件应该携带与事件发生时相关的上下文数据信息,但是并不是整个聚合根的状态数据,例如,在创建订单时可以携带订单的基本信息,而对于产品(Product)名称更新的
ProductNameUpdatedEvent
事件,则应该同时包含更新前后的产品名称:
public class ProductNameUpdatedEvent extends ProductEvent {
/**
* 更新前的名称
*/
private String oldName;
/**
* 更新后的名称
*/
private String newName;
}
发布领域事件
发布领域事件的几种方式
- 应用服务(ApplicationService)发布
- 资源库发布(Repository)发布
- 事件表方式
对比分析
- 1 & 2 方式需要综合考虑 数据库更新和事件发布之间的原子性(全部成功/全部失败),该场景之下,需要引入全局事务(Global Transaction/XA Transaction),但是全局事务本身的效率比较低,同时一些技术框架不支持全局事务
事件表方式,流程如下:
- 在更新业务表的同时,将领域事件一并保存到数据库的事件表中,此时业务表和事件表在同一个本地事务中,即保证了原子性,又保证了效率。
- 在后台开启一个任务,将事件表中的事件发布到消息队列中,发送成功之后删除掉事件。(消费方需要考虑幂等性)场景如下:
接受用户请求;
- 处理用户请求;
- 写入业务表;
- 写入事件表,事件表和业务表的更新在同一个本地数据库事务中;
- 事务完成后,即时触发事件的发送(比如可以通过Spring AOP的方式完成,也可以定时扫描事件表,还可以借助诸如MySQL的binlog之类的机制);
- 后台任务读取事件表;
- 后台任务发送事件到消息队列;
- 发送成功后删除事件。
消费领域事件
- 消费方的幂等性
- 消费方有可能进一步产生事件
事件驱动架构的2种风格
- 事件通知
- 发布方发布事件
- 消费方接收事件并处理
- 消费方调用发布方的API以获取事件相关数据
- 消费方更新自身状态
- 事件携带状态转移(Event-Carried State Transfer)
- 事件溯源