1.定义
领域事件 :在某某领域发生了某个一个动作或者产生了一个行为
2.领域事件的触发
领域事件由命令触发,触发命令可以是一个动作或者一个行为,例如前端的一个click点击事件或者数据库的变更触发binlog的行为。
3.构建并发布领域事件:
一个事件的基本属性至少包括如下:
3.1.唯一标识
(全局唯一,事件能够无歧义在多个限界上下文中传递,一般为聚合根的业务主键)
3.2.发生时间
3.3.事件类型
:对事件进行的一个分类或者说行为的描述,例如canal监听到一次数据库的变更事件,该事件可能是一个删除行为、或者新增行为、或者修改行为
3.4.事件源
: 触发事件的源头
3.5.业务属性
:记录事件发生那刻的业务数据,这些数据会随事件传输到订阅方,以开展后续业务操作
将以上事件属性聚合并发布事件。订阅方如果需要可以先存储事件,然后再将其转发到远程订阅方,或不经存储,直接转发,一般根据事件的复杂程度或者是否需要溯源来确定是否需要存储事件。
4.处理领域事件
4.1微服务内
领域事件发生在微服务内的聚合间,领域事件发生后完成事件实体的构建和事件数据持久化,发布方聚合将事件发布到事件总线,订阅方接收事件数据完成后续业务操作。
微服务内大部分事件的集成,都发生在同一进程
,进程自身即可控制事务。但一个事件若同时更新多个聚合,按一次事务只更新一个聚合原则
,可考虑引入事件总线。
例如分库分表的情况下,进行批量操作,就需要考虑事务一致性,这个过程会用到分布式事务,以保证发布方和订阅方的数据同时更新成功。
微服务内应用服务,可通过跨聚合的服务编排和组合,以服务调用方式完成跨聚合访问,这种方式通常应用于实时性和数据一致性要求高的场景。这个过程会用到分布式事务或事件总线,以保证发布方和订阅方的数据同时更新成功。在微服务内,推荐少用事件总线。
4.2 微服务间
跨微服务的领域事件会在不同限界上下文或领域模型间实现业务协作,主要为解耦
,减轻微服务间实时服务访问压力。
领域事件发生在微服务间较多,事件处理机制也更复杂。跨微服务事件可推动业务流程或数据在不同子域或微服务间直接流转。
跨微服务的事件机制要总体考虑事件构建、发布和订阅、事件数据持久化、MQ,甚至事件数据持久化时还可能需考虑引入分布式事务。
微服务间访问也可采用应用服务直接调用,实现数据和服务的实时访问,弊端就是跨微服务的数据同时变更需要引入分布式事务。分布式事务会影响系统性能,增加微服务间耦合,尽量避免使用。
4.3 事件总线
事件总线是对一个领域事件总体把控,提供事件分发和接收和聚合等。 是进程内模型,会在微服务内聚合之间遍历订阅者列表,采取同步或异步传递数据。
例如,例如上架一个新的sku,会触发到价格域、库存域、营销域等产生相应的变化,这个时候就需要事件总线进行统一把控,必须各域均处理成功,该sku才上架成功,否则需要协调各方统一回滚。
4.4 事件数据持久化
意义
实现发布方和订阅方事件数据的对账
当遇到MQ、订阅方系统宕机或网络中断,在问题解决后仍可继续后续业务流转,保证数据一致性。 毕竟虽然MQ都有持久化功能,但中间过程或在订阅到数据后,在处理之前出问题,需要进行数据对账,这样就没法找到发布时和处理后的数据版本。关键的业务数据推荐还是落库。
实现方案
持久化到本地业务DB的事件表,利用本地事务保证业务和事件数据的一致性
持久化到共享的事件DB。业务、事件DB不在同一DB,它们的数据持久化操作会跨DB,因此需分布式事务保证业务和事件数据强一致性,对系统性能有影响
4.5 领域事件来驱动业务的流转
领域事件在设计时我们要重点关注领域事件,用领域事件来驱动业务的流转,尽量采用基于事件的最终一致
,降低微服务之间直接访问的压力,实现微服务之间的解耦,维护领域模型的独立性和数据一致性。
5.示例:状态机上的领域事件
5.1现状
状态转移表:
事件:
状态转移机的值对象:
转移
状态机作用:根据事件+源状态=====>目标状态
前端应用对事件的触发分为保存触发和提交触发两种,后端应用在接受到事件之后,进行相应处理
场景1 :提交事件
后端接到提交事件之后,要区分出该事件的审批属性,根据是否走审批,将业务流转到不同的状态。而状态机为了区分出审批后的状态转移和不走审批的状态转移,将前端事件进行了拆分,将一个提交事件拆成2个:审批提交事件、不审批提交事件如下图:
这种设计方式,虽然把事件分类的属性提升到了事件的维度,即使总感觉哪里不舒坦,但也并无大碍
场景2:保存事件
业务:编制中、审批不通过、供应商确认不通过3中状态的数据均可编辑后保存,业务方的诉求是保存后数据状态的流转是:
编制中==>编制中
审批不通过==>审批不通过
供应商确认不通过==>供应商确认不通过
bug所在: 根据事件+源状态=====>错误状态
在现有设计的基础上如何改进呢?
一种方式就是,在业务代码里判断如果是保存事件的话,不走状态机,特殊处理保存事件的状态流转问题,此是现在使用方案
另一种是,根据场景1的经验,后端接到保存事件之后,想要区分出编制中、审批不通过、供应商确认不通过等状态的保存,只能将一个保存事件拆成3个保存事件,如下图
此方案需要在业务代码中感知单据状态,把本该状态机做的事情,让状态机的调用者做了一部分,完全违反了接口六大设计原则之迪米特法则(尽量不感知调用类里边的复杂逻辑,Law Of Demeter, LOD);虽然多了一步拆分事件的业务逻辑,但也能解决问题
场景3:保存事件
业务:编制中、审批不通过、供应商确认不通过3中状态的数据均可编辑后保存,业务方的诉求是保存后数据状态的流转是:
编制中==>编制中
审批不通过==>审批不通过
供应商确认不通过==>编制中
这样再强行拆分事件就显得非常别扭了,再进一步如果是在这些状态之前还要分审批或者不审批呢?岂不是拆起来更加麻烦!
为了解决这个问题,我们先回到设计状态机的初衷,我喜欢一句话,不管你走了多远,也不要忘记当初为何而出发。
状态机是为了实现在一个领域事件发生的时候,根据事件和当前状态能流转到下一个状态,但目前来看对一些特殊的场景支持不是很友好,容易出问题,根据上边3中场景也能发现我们一直的解决方案都是拆事件,对事件进行分类处理,但貌似我们领域事件的基本属性里边好像有一个属性是事件类型,一直没用到,何不尝试一下呢?
调整后的设计如下
领域事件模型
转移
根据事件+事件类型+源状态=====>目标状态
这样看起来领域事件的模型更加清晰,也能解决遇到的问题,但是对于提交事件来说,审批和非审批划为事件类型没什么问题,而对于保存的时候,将状态划为事件分类,总感觉不妥,因为状态机就是为了通过事件进行状态转移,而并非是状态区分事件,对状态机数据模型进一步优化如下:
Transfer也不要了,利用MultiKeyMap的多key属性实现事件+事件分类与状态的映射,领域事件模型的边界更加清晰,每个属性的职责也更清楚,状态更事件的概念也泾渭分明,更容易理解,最终状态机的职责就是发生某类事件,将数据状态从A流转到B,只要有事件发生,一定会产生状态的流转,无论是从A流转到B还是从A流转到A在业务代码都不需要感知
public class ReturnMaterialTransferTable2 {
public static ReturnMaterialTransferTable2 INSTANCE = new ReturnMaterialTransferTable2();
/**
* 状态转移表
* key1:事件
* key2:事件分类
* value:源事件与目标事件映射
*/
private static final MultiKeyMap<Object, Map<ReturnMaterialStatusEnum, ReturnMaterialStatusEnum>> transferMap = new MultiKeyMap<>();
static {
/**
* 采购商保存草稿
*/
transferMap.put(ReturnMaterialEventEnum2.PUR_SAVE, null,
new HashMap<ReturnMaterialStatusEnum, ReturnMaterialStatusEnum>() {
{
put(ReturnMaterialStatusEnum.EDITING, ReturnMaterialStatusEnum.EDITING);
put(ReturnMaterialStatusEnum.UNAPPROVED, ReturnMaterialStatusEnum.EDITING);
put(ReturnMaterialStatusEnum.UNCONFIRMED, ReturnMaterialStatusEnum.EDITING);
}
});
/**
* 采购商提交-不走审批
*/
transferMap.put(ReturnMaterialEventEnum2.PUR_SUBMIT, ReturnMaterialEventEnum2.Type.NO_AUDIT,
new HashMap<ReturnMaterialStatusEnum, ReturnMaterialStatusEnum>() {
{
put(ReturnMaterialStatusEnum.EDITING, ReturnMaterialStatusEnum.CONFIRMING);
put(ReturnMaterialStatusEnum.UNAPPROVED, ReturnMaterialStatusEnum.CONFIRMING);
}
});
/**
* 采购商提交-走审批
*/
transferMap.put(ReturnMaterialEventEnum2.PUR_SAVE, ReturnMaterialEventEnum2.Type.AUDIT,
new HashMap<ReturnMaterialStatusEnum, ReturnMaterialStatusEnum>() {
{
put(ReturnMaterialStatusEnum.EDITING, ReturnMaterialStatusEnum.EDITING);
put(ReturnMaterialStatusEnum.UNAPPROVED, ReturnMaterialStatusEnum.EDITING);
put(ReturnMaterialStatusEnum.UNCONFIRMED, ReturnMaterialStatusEnum.EDITING);
}
});
}
/**
* 获取目标状态
* @param event 状态迁移事件
* @param currentStatus 当前状态
* @return 目标状态
*/
public ReturnMaterialStatusEnum getTargetStatus(ReturnMaterialEventEnum2 event, ReturnMaterialEventEnum2.Type type,
ReturnMaterialStatusEnum currentStatus) {
if (!transferMap.containsKey(event, type)) {
return null;
}
Map<ReturnMaterialStatusEnum, ReturnMaterialStatusEnum> transfer = transferMap.get(event, type);
return transfer.get(currentStatus);
}
}