otk是什么意思(CQRS在DDD到底是什么)
admin
2023-08-21 17:25:16

作者:崔豪

来源:微信微信官方账号:石杉的建筑学笔记

产地:https://mp.weixin.qq.com/s? _ _ biz=mzu 0 otk 3 odq 3 ng==mid=2247494195 idx=1sn=725 d5df 4525 b 73365 c 6 e 71 b 73 e 9 c 72 c 7

介绍性歌曲(在弹词(弹词)和一些地方戏曲)

随着业务的不断发展,软件系统的架构也越来越复杂,但无论多么复杂的业务最终在系统中实现,无非就是读写操作。用户根据业务规则编写业务数据,然后根据查询规则获得想要的结果。一般来说,我们会说这些读写数据存储在一个数据库中,通过一套模型读写。而在大型系统中,查询操作往往比写操作多,于是就有了读写分离的思想,将读写操作的模型分开定义,提供不同的通道给用户使用。CQRS(Command-queryresponsibility segregation)就是基于这种思想的一种读写分离模式。今天我就围绕它给大家讲以下内容:

CQRS的演变与建筑——事件外包的原理与应用——事件外包与CQRS的完美结合——以CQRS为例——CQRS的演变与建筑

CQRS(Command-queryresponsibility segregation)是一种读写分离的模式。从字面上看,command就是命令的意思,代表写操作。Query的意思是查询,代表查询操作。这种模式的主要思想是将数据写操作和查询操作分开。

它起源于BertrandMayer设计的命令查询分离(CQS)原理。CQS声明一个类只能有两个方法:改变状态并返回void的方法和返回状态的方法。GregYoung是负责将这款车型命名为CQRS并进行推广的人。

首先,我们来看看在CQRS之前,系统中的修改和查询是如何处理的,如图1所示:

图1传统系统请求

传统的系统请求从最左边的客户端开始,通过右边红线的ApplicationService请求系统。在这里,ApplicationService可以理解为系统的门面,或者说控制器层负责接收客户端的请求。此时请求的内容比较简单,与数据库中的信息基本一致,所以这里使用DTO(DataTransferObject)进行直接请求。DTO通过DomainModel后直接到达数据库,沿着蓝线返回客户端。传统的请求方法对一些读操作和写操作使用相同的数据模型、一组DomainModel和相同的数据库。

从传统的操作来看,客户端的请求通过ApplicationService传递,用户的意图全部分解为CRUD操作,但无法在DomainModel中体现出来。为了保证d to的完整性和一致性,与操作无关的信息将并入DTO,查询操作和创建操作共用一个DTO,而域模型的业务流程被弱化。为了同时适应查询和创建操作,DTO在各个方面都进行了设计,这让它看起来有些臃肿。因此在传输中存在不必要的场转移。

此外,一个操作可以在DTO和域对象之间转换多次,这增加了系统的复杂性。另外,读写操作会围绕同一个数据模型,这对于读多写少的系统来说并不是效率最高的,尤其是以读操作为主的高并发系统。

由于传统系统架构中存在的这些问题,CQRS根据读写职责的不同将DomainModel分为两部分,即命令端和查询端。如图2所示,红线是命令端,对应的是域模型发送的向数据写入状态信息的命令操作指令。

作为查询操作,查询端用蓝线表示,通过QueryModel从数据库中获取信息,结果先通过左边的黑色返回给客户端。命令端和查询端都通过ApplicationService进入系统,共享同一个数据库,但命令端只写状态,查询端只读状态。

图2CQRS分为命令端和查询端。

目前读写操作已经分离。由于两种操作仍然共享同一个数据库,为了提高读写效率,分离数据库是必然的选择。如图3所示,原始数据库然后被分成WriterDatabase和ReaderDatabase,分别用于写和读。为了保证读写操作的数据一致性,需要在两个数据库之间进行数据同步。

因为数据同步是有时效性的,写入方是命令方,读取方是查询方,所以系统会智能保证最终的一致性。那么如何保证两个库的同步呢?下面需要介绍EventSourcing的概念。

EventSourcing的原理及应用

EventSourcing,也叫事件可追溯性,是MartinFowler提出的一种架构模型。其设计思想是系统中的业务由事件驱动。事件记录在系统中,反映了信息的状态。业务数据可以是事件生成的视图,没有必要保存在数据库中。

为了便于理解EventSourcing,我们将通过一个例子进一步解释,如图3所示:

图3命令端和查询端读写数据库的分离

我们从左向右看。对于商务舱& quot帐户& quot,有& quot属性& quot包括& quot帐户ID & quot和& quot帐户金额& quot信息,并拥有& quot方法& quot包括& quot创建帐户& quot和& quot创建帐户& quot

存现金”和“取现金”。中间绿色的事件序列,是针对“账户”进行的一些列操作,按照其中的序列号来看。

1.创建了一个银行账户,假设此时的账户ID为“0001”。

2.针对“0001”这个账户存入300元现金。

3.然后从“0001”这个账户取出100元现金。

4.最后,再存入200元。

上面生成的这一系列事件会保存到下方的EventStore的事件库中,这里并不会保存“账户”的状态信息。当需要获取“账户”数据的时候,会通过这些事件信息,还原成“账户”的最终状态,也就是“账户ID”为“0001”,“账户金额”为400。其具体实现方式是,通过账户相关的四个事件对应的处理方法,重新生成当前状态。如果每次查询状态信息都需要这样处理势必会造成资源的浪费,因此在右侧黄色的部分,我们将最终的“账户”信息通过视图的方式保存下来,以供查询。

图3EventSourcing实例图

上面这个“账户”处理的过程,就是EventSourcing,说白了就是通过事件的处理模式。它将系统中的操作都按照事件的方式记录并保存,任何实体的最终状态都是通过事件的叠加和还原确认的。

EventSourcing包含的内容

上面介绍了EventSourcing的执行原理和基本概念,这里一起来看看其包含的主要内容,便于我们对它有更加全面的理解。

聚合对象:图3的例子中“账户”就是一个聚合对象,它里面包含“账户ID”、“账户金额”等的基本信息,也包含了对账户操作的方法:“创建账户”、“存现金”、“取现金”。同时“账户”在领域驱动开发中对应的是一个领域模型。

EventStore:在EventSourcing模式中,事件所保存的数据库称为EventStore。在事件中需要包含聚合对象的ID,以及事件的顺序。这样在查询的时候可以根据聚合ID从数据库中找到相关的事件,并通过事件的序号还原执行顺序。也就是事件的重现,也就是某一时刻执行的事件取出来,调用他的处理函数,还原那个时间点的业务状态。为了获取最新的“账户”状态信息,需要通过EventSourcing中获取对应的事件进行回放,从而获取当前的状态,这样的操作会浪费很多资源。因此我们会将聚合对象的最新数据状态,写到一个表中,这个表就是视图。又或者将这个状态信息发送给其他的应用程序进行后续的业务操作。查询的内容是针对“账户”最终状态的,因此针对的对象应该是视图。这里的设定刚好的CQRS中的读写分离不谋而合,通过EventStore存放Command端的Event信息,通过视图存放实体最终状态的信息,而Query端从视图查询数据返回给用户。

EventSourcing的优缺点

上面介绍了EventSourcing的原理和内容以后再来看看它的优缺点。

EventSourcing的优点:

溯源事件与重现操作:特别是在业务复杂的系统中,一个事务包含多个操作,它们有的是并行有的串行,如果需要了解操作的执行就需要对每个事件了如指掌。EventSourcing恰恰提供了事件的历史信息,方便查找任何时间点发生的事情。追踪和修复Bug:可以通过事件分析业务的执行过程,帮助发现Bug,例如重方Bug产生时的事件序列,从而定位Bug所处位置。发现Bug并且修复以后,可以通过重新聚合业务数据,重放执行的事件序列验证修复结果,同时将Bug造成的损失进行挽回。提高性能:EventSourcing模式下,由于是记录事件执行的序列,因此都是新增操作,没有更新操作,相对于需要更新操作的系统而言记录数据的性能是提高了。如果使用视图的方式将实体的最终状态可以传递给其他的应用,而不用写入数据库以后再读取,这种做法也提高了效率。

EventSourcing的缺点:

转变思路:EventSourcing的落地需要在设计时就用领域驱动的方式开展,需要有基于事件的响应式编程思维。这种方式需要以领域模型设计优先,而不是传统的数据库设计优先。变更事件结构:随着业务流程的变化需要不断调整事件结构,对事件添加或者修改一些数据。这种行为会影响到“历史重现”,需要考虑兼容之前的事件结构。处理幂等事件:如果对应的事务在执行过程中被中断,需要通过事件回放的方式达到事务的最终一致性问题。此时需要对事件的幂等性提出要求,也就是同一个事件运行多次得到的结果不变。需要在事件处理时丢弃重复事件。查询事件数据库(eventstore):由于数据库中存放的一个个事件,如果针对实体状态的查询会相对困难。需要将这些事件重放,获取最新的实体状态的信息。这也是为什么需要通过CQRS的方式将读写进行分离,Command端使用EventSourcing而Query端使用EventSourcing发出Event的最终状态进行查询的原因。

CQRS与EventSourcing的完美结合

通过上面对EventSourcing的介绍,可以发现它针对Event进行记录存放到EventStore中,并且把最终的状态放到视图中进行保存可以供给Query端进行查询。这种模式天生与CQRS就有默契的配合。

从CQRS模式的结构看,实体状态的变化发生在Command端,Command端知道业务处理进行了哪些具体操作,将这些具体的操作进行封装就形成了Event。

而Query端,查询返回的是实体当前状态状态。根据“当前状态+变化=新的状态”,如果能从Command端得到“变化”,再加上Query端自身获取的“当前状态”就能得到变化后的“新的状态”。

此时Command端发出的Event正好符合这个“变化”,如果当变化发生也就是新Event产生时,由Command端将这个Event推送到Query端,Query端根据Event刷新状态,就能保证两端实体状态一致,达到最终一致性,如图4所示:

图4EventSourcing和CQRS结合

在图3的基础上加入EventHandler也就是图中蓝色部分,这部分接收从DomainModel中发过来的Event信息,也就是最新的实体修改信息。再将这个信息存放到ReaderDatabase(也可以理解为视图)中,这样新的Event信息加上当前的实体信息就时最新的实体信息了。而采用这种方式以后Query端依旧可以通过ReaderDatabase获取数据对其原来的操作并没有产生影响。

再回到Command端,其对应的多次操作的Event会存放到EventStore中,作为业务跟踪的记录被保存下来。

上面提到的只是一种系统架构的模式,在实际运用中可以根据具体情况进行改进和优化。如图5所示,可以在Command端和Query端进行Event交换的时候加入队列,满足两套应用程序部署在不同进程的场景需求。

图5Command端和Query端加入队列

一个CQRS的例子

上面聊到了CQRS与EventSourcing的完美结合,这里通过一个例子给大家进一步介绍其运作的过程。这个例子的背景是,对于用户(User)而言保存了对应的联系方式(Contact)和住址(Address)。

Command用来建立(Create)用户(User)和更新(Update)用户(User);Query用来查询用户(User)对应的住址(Address)和联系方式(Contact)。

如图4所示,Client请求应用分为上线两条线,分别用四种颜色代表。我们根据不同颜色来讲解Command端和Query端执行的过程。

图4EventSourcing和CQRS结合

红色向左的线:这里主要是针对User的create和update操作,分别填充CreateUserCommand类和UpdateUserCommand类,作为UserAggregate聚合类的输入参数。在UserAggregate中分别由,handleCreateUserCommand和handleUpdateUserCommand两个方法处理,最后通过UserWriteRepository来保存到Writedatabase中。

绿色向下的线:其连接了紫色的区域是UserProjection,它的作用是将Writedatabase的数据同步到Readdatabase中。蓝色向右的线:Client发起Query请求通过AddressByRegionQuery类和ContactByTypeQuery类构建请求,将其传送到UserProjection类进行处理,其中handle方法分别对两类参数的请求进行处理。最后通过UserReadRepository获取Readdatabase中的信息。紫色向左的线:当从Readdatabase中获取信息以后,返回给Client。

图6CQRS例子图解

在了解了整体架构以后再来看看具体实现的类结构。

如图7所示,User实体类包括如下几个字段,也就是我们要操作的业务实体。包括用户的基本信息,其中contact和address类的具体信息在这里不展开描述。

图7User实体类

Command的类信息如图8所示,其内容相对简单。针对CreateUserCommand主要用于创建用户,包括UserID和FirstName以及LastName。

图8CreateUserCommand类

如图9所示,UpdateUserCommand中加入了地址和联系方式的更新内容。

图9UpdateUserCommand类

有了Command再来看看聚合类UserAggregate,由于其中包括Create和Update的处理方法,这里介绍其中的handleCreateUserCommand方法,也就是处理新建用户命令。

这里会创建一个UserCreatedEvent对象,并将其通过WriteRepository保存到Writedatabase中。也就是在ES中的Eventstore,同时会将event的list返回。

图10handleCreateUserCommand类

在处理完Command以后会返回Event,这个Event在保存到数据库中的同时,也会发送和Query端作为最新的实体状态进行更新,这里会用到UserProjector类完成映射。如图11,所示,其中的project方法会针对UserID的events进行逐一处理。

图11UserProjector类

看完了Command端和同步的Projector,再来看看Query端的类。如图12所示,AddressByRegionQuery类定义了UserID和State信息。

图12AddressByRegionQuery类

如图13所示,ContactByTypeQuery定义了UserID和ContactType的信息。

图13ContactByTypeQuery类

如图14所示,上面提到的AddressByRegionQuery和ContactByTypeQuery作为参数传入到UserProjection类的handle方法中,并且返回对应的Contact和Address信息。使用了UserReadRepositiory从Readdatabase中获取数据。

图14UserProjection

最后,再来看看测试代码这里将其分为7个步骤,如图15所示。

随机生成用户ID。

通过CreateUserCommand,创建新建用户的Command,并且通过UserAggregate生成对应的事件。通过UserProjector将事件映射到Query端的数据库中。通过UpdateUserCommand,创建更新地址信息的Command,生成对应的事件。通过UserProjector将事件映射到Query端的数据库中。通过AddressByRegionQuery,创建查询地址信息的Query。执行查询从Readdatabase中获取数据与假设值进行比较。

图15Command和Query的执行过程

最后来看看这些文件的目录结构,如图16所示。

图16文件结构

总结

本文从CQRS的演变切入,介绍了如何从“读写一体”过渡到“读写分离”的CQRS的架构方式,以及CQRS方式的几种表现形式。通过读数据库与写数据库之间同步的问题,引出EventSourcing的原理和应用,包括EventSourcing的内容和优缺点。从而得出CQRS与EventSourcing结合完成读写分离的结论。最后,通过一个CQRS的例子带大家从代码的角度走了一遍CQRS的流程。

作者:崔皓

来源:微信公众号:石杉的架构笔记

出处:https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&mid=2247494195&idx=1&sn=725d5df4525b73365c6e71b73e9c72c7

相关内容

热门资讯

金花创建房间/微信金花房卡怎么... 1.微信渠道:(荣耀联盟)大厅介绍:咨询房/卡添加微信:88355042 2.微信游戏中心:打开微...
金花房间卡/金花房卡如何购买/... 金花房间卡/金花房卡如何购买/新超圣金花房卡正版如何购买新超圣是一款非常受欢迎的游戏,咨询房/卡添加...
牛牛创建房间/金花房卡批发/神... 微信游戏中心:神牛大厅房卡在哪里买打开微信,添加客服【88355042】,进入游戏中心或相关小程序,...
链接牛牛/牛牛房卡游戏代理/鸿... 鸿运大厅房卡更多详情添加微:33549083、 2、在商城页面中选择房卡选项。 3、根...
科技实测!牛牛房卡怎么获得/乐... 微信游戏中心:乐酷大厅房卡在哪里买打开微信,添加客服【88355042】,进入游戏中心或相关小程序,...