ENode 2.6 - 关于聚合根一次产生多个领域事件时更新读库的方案设计

最近在用ENode开发conference案例时,遇到一个问题,写篇文章分享一下。

问题背景

Conference案例,是一个关于在线创建会议(类似QCon这种全球开发者大会)、在线管理会议位置信息、在线预订某个会议的位置的,这样一个系统。具体可以看微软的这个项目的主页:http://cqrsjourney.github.io。

然后我们设计了一个Conference聚合根,对应领域中的会议这个领域概念。Conference聚合根下面,有一些位置信息SeatType。一个会议聚合根下面可以添加不同类型的位置,每种类型的位置可以指定数量以及价格。所以,Conference是聚合根,Conference本身有一些我们所关心的基本属性,同时它内部聚合了一些SeatType子实体。每个SeatType包含了位置的价格、数量这两个信息。

然后,在UI层面,我们会有如下界面边界管理一个会议的所有位置信息。

上图列出了某个会议的两类位置,Quota表示位置的配额数量;当我们要修改某种位置时,可以点击链接,然后出现如下图所示:

出现四个编辑框,我们可以修改任何一个框。修改完后点击保存,我们就能更新某个类型的位置信息了。然后,我们在domain里,设计了两个domain event;分别表示位置基本信息改变和位置配额数量的改变。

为什么要独立出数量改变的domain event呢?因为当用户在前台下单订购位置时,这个数量也会变化。也就是位置数量可能会单独变化。所以,我们考虑单独为位置数量的变化定义一个domain event。

然后,我们目前的代码是,当点击保存时,首先更新会更新位置的基本信息,然后判断数量是否有变化,如果没变化,则只产生位置基本信息变化的domain event;如果有变化,则同时产生位置数量改变的domain event。Conference聚合根相关方法的具体实现如下:

上面的代码的大致意思是,先从聚合内找出需要修改的位置类型,如果不存在就抛异常;如果存在,则先产生位置基本信息的改变事件;然后判断数量是否有变化,如果有变化,则继续判断当前输入的数量是否太小,如果太小也是不允许的。

比如,假如用户录入的数量是10,但是当前这种类型的位置已经有11个被预定了,那就不能改为10,而是必须至少为11。最后,如果一切都合法,就产生一个SeatTypeQuantityChanged的事件,表示某个类型的位置的数量发生了变化,同时在事件中带上可预定的剩余位置的数量。

然后读库我们就根据上面这两个事件来更新。

现在的问题是,假如两个事件都发生了,那读库要怎么原子更新(在一个事务里更新)?我们的一个event handler只能处理一个event;也就是说,我们会有两个event handler,分别处理对应的事件。由于domain aggregate是一次性原子的方式同时产生两个domain event。所以,我们要确保两个event handler要么都更新成功,要么都不更新成功,这个问题之前没考虑到过,下面我们来想想办法。

解决思路

思路1

想办法把这两个event handler包装在一个事务里,但这要求框架支持这样的跨多个event handler的事务机制;对框架要求的的改造有点大,复杂度高,不太可行。因为框架要考虑的问题是要更通用的,比如,一旦引入事务,也许还会引入分布式事务等问题。而且这种做法,性能也不高,违反ENode一开始就是为高并发设计的初衷。

思路2

要求领域里不要设计两个domain event了,就用一个domain event解决;这个event包含所有信息的修改,包括数量的修改。这个办法可行,但要求模型做出妥协和让步了。假如有一天我们遇到模型必须要产生多个事件的情况,那怎么办呢?所以,这个思路还是在逃避问题。

思路3

不采用事务,而是采用乐观锁+顺序控制+幂等支持的方式解决问题。思路是,框架按照顺序调用这两个event handler,调用的顺序和这两个事件的顺序一致;两个event handler允许不在一个事务里。

这样的问题是,假如第一个事件处理成功了,然后此时机器断电了,第二个事件没被处理,怎么办?那就是要做到,当下一次机器重启后,第二个事件能被处理。然后,因为整个架构是分布式的,所以第一个事件也是有可能被重复处理的,框架在调用event handler时,为了性能方面的考虑,只会尽量保证同一个event不会被同一个event handler重复处理,不会绝对保证;但是框架有提供机制,让开发人员在event handler内部通过依赖版本号的方式来解决重复处理的问题。所以,总结一下,我们需要处理的问题有以下3个:

  1. 需要保证任何event handler内部自己能做到绝对的幂等,框架提供支持;
  2. 需要保证任何一个event至少被处理一次,即便是在任何时候断电的情况下;
  3. 需要保证同一个事件流里的事件,处理的顺序也要按照事件流的顺序处理;

为了做到上面这3点,我对ENode做了一个完善,就是为事件引入了一个子版本号的概念。

就是当聚合根每次做出修改后,不管产生多少个domain event,这些domain event都是在一个event stream里;每个event stream都有一个版本号,然后每个domain event的主版本号就是其所在的event stream的版本号。比如某个聚合根某次变化产生了2个domain event,它们被保证在一个event stream里,然后假如这个event stream的版本号为10,那每个domain event的主版本号也是10;这点ENode框架可以做保证。那event stream的版本号哪里来的呢?就是从聚合根上得来,因为每个聚合根都维护了当前自己的版本号是什么,用version表示,那它下一次产生的event stream的版本号就是version+1。

上面解释了什么是事件的主版本号。下面我们在说一下什么是事件的子版本号。子版本号比较简单,就是假如一个event stream里包含2个事件,那第一个事件的子版本号是1,第二个则是2;所以,其实子版本号就是事件在事件流里的顺序号。

然后,有了事件的主版本号和子版本号的概念。我们就可以做到上面的3点要求了。其中的第2点,EQueue会做到确保任何一个消息至少被处理一次,这里不做展开了。第1、3点,我们通过下面的代码结合分析讨论。

为了代码效果好一点,我直接通过截图的方式了,博客园以后官方提供一套这样的代码模板吧,呵呵。@蟋蟀,上次你跟我说的那个模板,我后来忘记使用了:)

上面的代码中,每个event handler内部有一个事务,为什么还需要事务?因为我们现在更新的是聚合根,子实体(位置信息)是聚合根的一部分;所以读库更新时,自然也要更新聚合根本身的。只不过这里只需要更新聚合根的版本号即可。

第一个event handler,我们先启动一个事务,然后先更新聚合根的主版本号,以及次版本号;假如数据库里conference记录的当前的主版本号是10,次版本号是1,那这个evnt.Version就是11,evnt.Sequence是1,Sequence就是次版本号。然后通过第一条Update SQL我们就能更新聚合根的主版本号以及次版本号了。由于单条update sql是原子事务(无并发问题)的,所以我们只要判断更新的影响行数是否为1。如果是1,则说明更新成功,那就可以更新位置那条记录了。然后,由于这两条更新语句在一个事务里,所以要么全部完成,要么什么都不做,不会有做了一半的情况。

第二个event handler,同样,我们也是先启动一个事务。然后区别是,因为我们知道SeatTypeQuantityChanged事件和SeatTypeUpdate事件总是在一个事件流里发生的,且它总是位于第二个顺序。所以,当这个event handler被执行时,聚合根的主版本号一定已经是11了,且子版本号是1。那么,我们在第二个event handler中,对聚合根,只需要更新子版本号为2即可。就是第一个Update语句。然后同样判断影响行数是否为1。如果是,则更新位置的数量以及可用数量;如果不是1,则什么都不做。

有一个问题,什么时候会出现不是1呢?就是在这个event handler被重复执行的时候。这种情况,我们忽略即可。因为我们就是为了要做到update的幂等处理。

到这里基本差不多了。但是还需要说明一个大前提。就是上面这个大家可以看到,第一个event handler里,更新聚合根的主版本号时,where条件里会判断聚合根记录的当前版本号是evnt.version - 1;这个就是为了保证,读库更新时,总是按照domain event的发生顺序依次更新的,不能跳过更新,也不能乱序。否则读库的最终数据就不一致了。所以,event handler内部要做这样的判断,确保绝对不会发生这样的事情。但光event handler内部判断还不够。ENode框架也要保证event stream消息的处理顺序也是这样依次按照顺序的,否则event handler里聚合根更新的影响行数也许永远都不能为1了。

ENode已经意识到这个问题,所以已经帮我们做了这样的保证!

总结

上面的最后一个方案,我觉得是比较通用的解决方案。框架不需要做支持跨event handler的事务,改动比较小。同时还能保证读库更新的性能,另外,在断电的时候,也能保证事件被处理。

总之,一切的一切都是为了高性能、为了保证最终一致性。又花了一篇文章分享了一点小小的设计,呵呵。

时间: 2024-10-24 02:22:29

ENode 2.6 - 关于聚合根一次产生多个领域事件时更新读库的方案设计的相关文章

领域模型:聚合、聚合根详解

聚合和聚合根是领域模型里面很重要的一个概念,其实我们在从真实世界对业务对象进行识别和概念建模的时候,关注的就是聚合根,这才是我们真正要管理的业务对象.一个对象可能有多个层次,也可能有多个子实体,但是这些子实体都不可能孤立存在,它们必须依附于一个聚合根存在,它们和根节点具有同样的生命周期. 如果一个客户消亡,客户联系方式,客户的多张银行账户信息将不再有任何意义.如果一张采购订单头消失,那么采购订单明细没有任何存在的意义.客户,采购订单,发票这些从真实业务中转化过来的业务对象才是真正的领域核心对象.

领域驱动设计之聚合与聚合根实例一

通过一个实例来说明如何划分聚合与聚合根 场景:一个下订单的业务,一个订单必须有相应的客户信息,订单下有订单项,每个订单项必须有相应的产品信息,产品有分类的信息. 1.根据这个基本的需求,我们初步确定的实体.值对象与关联关系为(这里采用EF的Model First): 2.经过业务深入分析,以及聚合与聚合根确定原则,最终我们确定的聚合与聚合根是(红色代表聚合根,蓝色代表聚合内的实体,灰色代表值对象): 划分与确定理由1.订单.客户与产品都可以在不同的领域被独立访问到,所以应该是属于不同聚合的聚合根

领域驱动设计案例之领域层实体与聚合根实现

在领域层中,可以实现实体与聚合根的业务逻辑,在实现业务逻辑之前,我们首先要确定实体和聚合根的一些基本行为,比如判断实体是否相等.关于领域对象的具体业务逻辑实现,因为涉及到要与数据库交互,所以等看完仓储的实现后,再实现领域对象的业务逻辑. using System; using Order.Domain.Aggreate; namespace Order.Domain.Model { public abstract class Entity : IEntity { private Guid id

004-spring-data-elasticsearch 3.0.0.0使用【二】-spring-data之定义方法、创建repository实例、从聚合根发布事件

续上文 1.4.定义方法 存储库代理有两种方法可以从方法名称派生特定于存储的查询.它可以直接从方法名称派生查询,或者使用手动定义的查询.可用选项取决于实际store.但是,必须有一个策略来决定创建什么样的实际查询.我们来看看可用的选项. 1.4.1.查询策略 以下策略可用于存储库基础结构来解析查询.在配置XML配置的情况下,您可以通过query-lookup-strategy属性在命名空间配置策略,或者在Java配置的情况下通过Enable $ {store}存储库注释的queryLookupS

ddd 聚合根 之 聚合与不聚合 设计

聚合 不聚合 订单和订单明细 论坛主贴与贴子回复 订单和收货地址(vo)  

DDD实践问题之 - 关于论坛的帖子回复统计信息的更新的思考

之前,在用ENode开发forum案例时,遇到了关于如何实现论坛帖子的回复的统计信息如何更新的问题.后来找到了自己认为比较合理的解决方案,分享给大家.也希望能和大家交流,擦出更多的火花. 论坛核心领域问题分析 论坛领域的核心概念是:帖子.回复.大家都知道,一个帖子可以有零个或多个回复.对同一个帖子,不同的人可以并行发表回复.回复发表后,查看帖子详情时,可以根据回复的发表时间排序显示:此外,我们还关心某个帖子的最新发表的回复.最新回复的作者.最新回复时间,以及总回复数. 我们设计的系统,应该在实现

Event Driven Architecture

在微服务中使用领域事件 稍微回想一下计算机硬件的工作原理我们便不难发现,整个计算机的工作过程其实就是一个对事件的处理过程.当你点击鼠标.敲击键盘或者插上U盘时,计算机便以中断的形式处理各种外部事件.在软件开发领域,事件驱动架构(Event Driven Architecture,EDA)早已被开发者用于各种实践,典型的应用场景比如浏览器对用户输入的处理.消息机制以及SOA.最近几年重新进入开发者视野的响应式编程(Reactive Programming)更是将事件作为该编程模型中的一等公民.可见

如何运用领域驱动设计 - 领域事件

开篇 距离发布上一篇该系列的文章好像已经过了快一个半月了,好吧,我托更了??.一晃就已经到了3月份,在这樱花??盛开的季节,终于得重新连载该系列了.在停更的期间时不时会收到大家关于DDD的留言和问题,一旦我有时间一定会回复大家的问题.在此,衷心感谢大家对本系列文章的支持??. 概述 在实践领域驱动设计(DDD)的过程中,我们往往会遇到多个领域对象相互交互的情况.比如聚合根A在执行某操作之前需要得到聚合根B的某个信号(或某些数据).如果在单体应用程序中,我们有条件和机会使得两者进行强引用来完成操作

ENode框架Conference案例分析系列之 - 事件溯源如何处理重构问题

前言 本文可能对大多数不太了解ENode的朋友来说,理解起来比较费劲,这篇文章主要讲思路,而不是一上来就讲结果.我写文章,总是希望能把自己的思考过程尽量能表达出来,能让大家知道每一个设计背后的思考的东西.我觉得,任何设计的结果可能看起来很高大上,一张图即可,但背后的思考,才是更有价值的东西. 本篇文章想写一下ENode如何处理由于业务需求的变化而导致的模型重构的问题.DDD之所以能解决复杂的业务问题是因为DDD是一种模型驱动的软件设计方法.用领域模型来捕获业务需求,根据业务需求,抽象出满足需求的