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-09-06 11:06:09

ENode框架Conference案例分析系列之 - 复杂情况的读库更新设计的相关文章

ENode框架Conference案例分析系列之 - 架构设计

Conference架构概述 先贴一下Conference案例的在线地址,UI因为完全拿了微软的实现,所以都是英文的,以后我有空再改为中文的. Conference后台会议管理:http://www.enode.me/conference Conference前台预定座位:http://www.enode.me/registration ENode论坛开源案例:http://www.enode.me/post ENode开源项目地址:https://github.com/tangxuehua/e

ENode框架Conference案例分析系列之 - Quick Start

前言 前一篇文章介绍了Conference案例的架构设计,本篇文章开始介绍Conference案例的代码实现.由于代码比较多,一开始就全部介绍所有细节,估计很多人接受不了,也理解不了.所以,我先进行一次QuickStart的介绍,即选取某个简单典型的场景从前到后过一下每个环节.这样大家就能够快速对代码的重要关键环节有大概的理解.另外,我现在正在做ENode的官网,到时会像axon framework一样,介绍ENode框架本身.使用场景.性能数据.案例,以及论坛社区等功能: 本文打算选择Conf

ENode框架Conference案例分析系列之 - 上下文划分和领域建模

前面一片文章,我介绍了Conference案例的核心业务,为了方便后面的分析,我这里再列一下: 业务描述 Conference是这样一个系统,它提供了一个在线创建会议以及预订会议座位的平台.这个系统的用户有两类:1)客户,可以创建和管理会议:2)会议座位预定者,可以预订会议座位.具体的关键业务描述如下: 客户创建一个会议,并录入会议的基本信息,比如名称.时间段.地点,等:会议创建后,系统会为客户自动生成一个AccessCode,客户可以通过AccessCode访问自己创建的会议: 客户定义某个会

ENode框架Conference案例分析系列之 - 业务简介

前言 ENode是一个应用开发框架.通过ENode,我们可以方便的开发基于DDD+CQRS+EventSourcing+EDA架构的应用程序.之前我已经写了很多关于ENode的架构以及设计原理的文章,但是因为没有和具体的例子结合来进行分析,所以可能很多人还是无法理解ENode的功能和设计.所以,接下来,我想通过一个较为完整的案例来一步步从业务分析到领域模型设计再到代码实现,以案例的方式讲解ENode如何帮助我们落实DDD的编码实现. 本文是这个系列的第一篇,所以需要先介绍这个案例的一些业务. 前

日本运营商数据业务发展策略案例分析报告

易观国际近期发布<运营商数据业务海外http://www.aliyun.com/zixun/aggregation/7734.html">案例分析系列-日本运营商数据业务发展策略案例分析报告>数据显示,日本移动互联网市场启动时间较早,且发展极为迅速,在2000年其移动互联网用户总数就已经超过了全国移动用户总数的50%.以NTT DoCoMo为例,截至2007年底,其移动互联网用户数达到4757万,占其总用户数的90.41%,稍高于国家整体移动互联网用户占比.易观国际分析认为,日

《OSPF网络设计解决方案(第2版)》一2.7 案例分析:使用链路状态数据库

2.7 案例分析:使用链路状态数据库 OSPF网络设计解决方案(第2版) 在本章之前的内容中,我们已经学习到了如何使用LSA在OSPF路由器之间发送有关链路的信息.这些LSA被存储于路由器内部的一个数据库中,并且一条LSA将作为该数据库的一条记录. 图2-16给出了本节案例分析所使用的OSPF网络拓扑. 例2-6显示了在HAL9000路由器上使用show ip ospf database命令的输出条目. 注意这里的输出并未包含图2-16中其他区域的信息(即只有区域0的条目),这是因为路由器HAL

《容器技术系列》一1.4 Docker运行案例分析

1.4 Docker运行案例分析 1.3节着重介绍了Docker架构中各个模块的功能,学完后我们可以对Docker的架构有一个宏观的认识.熟悉一款软件,研究一个系统,从静态的角度认识架构的各个模块,仅仅是第一步:从动态的角度,掌握软件或者系统的运行原理,即熟知架构中模块间的通信逻辑,无疑会让自己对软件或系统的理解更上一层楼.本节将从实际的Docker运行案例出发,串联Docker各模块,从而学习Docker的运行流程.分析原型为Docker中的docker pull与docker run两个命令

enode框架入门:事件驱动架构(EDA)思想的在框架中如何体现

开源地址:https://github.com/tangxuehua/enode 上一篇文章,我给大家分享了我的一个基于DDD 以及EDA架构的框架enode,但是只是介绍了一个大概.接下来我准备用很多一篇篇详细但不冗长的文章介绍每 个点.尽量争取一次不介绍太多内容,但希望每次介绍完后都能让大家知道这个小点的设计思想,以及为了解 决的问题. 好了,这篇文章,我主要想介绍的是EDA思想在enode框架中如何体现? 经典DDD的基 于领域服务的实现方式 一般的应用程序,如果一个用户动作会涉及多个聚合

enode框架入门:开篇

前言 今天是个开心的日子,又是周末,可以安心轻松的写写文章了.经过了大概3年的DDD理论积累 ,以及去年年初的第一个版本的event sourcing框架的开发以及项目实践经验,再通过今年上半年利用业余时 间的设计与开发,我的enode框架终于可以和大家见面了. 自从Eric Evan提出DDD领域驱动设计以来已 经过了很多年了,现在已经有很多人在学习或实践DDD.但是我发现目前能够支持DDD开发的框架还不多,至少 在国内还不多.据我所知道的java和.net平台,国外比较有名的有:基于java