DDD 领域事件

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现状
状态转移表:

image.png

事件:
image.png

状态转移机的值对象:
image.png

转移
状态机作用:根据事件+源状态=====>目标状态

前端应用对事件的触发分为保存触发和提交触发两种,后端应用在接受到事件之后,进行相应处理
场景1 :提交事件
后端接到提交事件之后,要区分出该事件的审批属性,根据是否走审批,将业务流转到不同的状态。而状态机为了区分出审批后的状态转移和不走审批的状态转移,将前端事件进行了拆分,将一个提交事件拆成2个:审批提交事件、不审批提交事件如下图:

image.png

这种设计方式,虽然把事件分类的属性提升到了事件的维度,即使总感觉哪里不舒坦,但也并无大碍
场景2:保存事件
业务:编制中、审批不通过、供应商确认不通过3中状态的数据均可编辑后保存,业务方的诉求是保存后数据状态的流转是:
编制中==>编制中
审批不通过==>审批不通过
供应商确认不通过==>供应商确认不通过

bug所在: 根据事件+源状态=====>错误状态

image.png

在现有设计的基础上如何改进呢?
一种方式就是,在业务代码里判断如果是保存事件的话,不走状态机,特殊处理保存事件的状态流转问题,此是现在使用方案
另一种是,根据场景1的经验,后端接到保存事件之后,想要区分出编制中、审批不通过、供应商确认不通过等状态的保存,只能将一个保存事件拆成3个保存事件,如下图
image.png

此方案需要在业务代码中感知单据状态,把本该状态机做的事情,让状态机的调用者做了一部分,完全违反了接口六大设计原则之迪米特法则(尽量不感知调用类里边的复杂逻辑,Law Of Demeter, LOD);虽然多了一步拆分事件的业务逻辑,但也能解决问题
场景3:保存事件
业务:编制中、审批不通过、供应商确认不通过3中状态的数据均可编辑后保存,业务方的诉求是保存后数据状态的流转是:
编制中==>编制中
审批不通过==>审批不通过
供应商确认不通过==>编制中
这样再强行拆分事件就显得非常别扭了,再进一步如果是在这些状态之前还要分审批或者不审批呢?岂不是拆起来更加麻烦!
为了解决这个问题,我们先回到设计状态机的初衷,我喜欢一句话,不管你走了多远,也不要忘记当初为何而出发。
状态机是为了实现在一个领域事件发生的时候,根据事件和当前状态能流转到下一个状态,但目前来看对一些特殊的场景支持不是很友好,容易出问题,根据上边3中场景也能发现我们一直的解决方案都是拆事件,对事件进行分类处理,但貌似我们领域事件的基本属性里边好像有一个属性是事件类型,一直没用到,何不尝试一下呢?

image.png

调整后的设计如下


image.png

领域事件模型


image.png

转移
根据事件+事件类型+源状态=====>目标状态

这样看起来领域事件的模型更加清晰,也能解决遇到的问题,但是对于提交事件来说,审批和非审批划为事件类型没什么问题,而对于保存的时候,将状态划为事件分类,总感觉不妥,因为状态机就是为了通过事件进行状态转移,而并非是状态区分事件,对状态机数据模型进一步优化如下:


image.png

Transfer也不要了,利用MultiKeyMap的多key属性实现事件+事件分类与状态的映射,领域事件模型的边界更加清晰,每个属性的职责也更清楚,状态更事件的概念也泾渭分明,更容易理解,最终状态机的职责就是发生某类事件,将数据状态从A流转到B,只要有事件发生,一定会产生状态的流转,无论是从A流转到B还是从A流转到A在业务代码都不需要感知
image.png
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);
    }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容