共计 7159 个字符,预计需要花费 18 分钟才能阅读完成。
这篇文章主要介绍“DDD 里面的 CQRS 是什么”,在日常操作中,相信很多人在 DDD 里面的 CQRS 是什么问题上存在疑惑,丸趣 TV 小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”DDD 里面的 CQRS 是什么”的疑惑有所帮助!接下来,请跟着丸趣 TV 小编一起来学习吧!
开篇
随着业务不断发展,软件系统的架构也越来越复杂,但无论多复杂的业务最终在系统中实现的时候,无非是读写操作。用户根据业务规则写入商业数据,再根据查询规则获取想要的结果。通常而言我们会讲这些读写的数据放到一个数据库中保存,通过一套模型对其进行读写操作。而在大型系统中往往查询操作远远多于写入操作,于是就有了读写分离的思想,将读操作和写操作的模型分开定义并且提供不同的通道供用户使用。CQRS(Command-Query Responsibility Segregation) 就是基于这一思想提供的一种模式读写分离的模式,今天就围绕着它给大家讲述以下内容:
CQRS 的演变和架构
Event Sourcing 原理与应用
Event Sourcing 与 CQRS 的完美结合
CQRS 的例子
CQRS 的演变和架构
CQRS(Command-Query Responsibility Segregation) 是一种读写分离的模式,从字面意思上理解 Command 是命令的意思,其代表写入操作;Query 是查询的意思,代表的查询操作,这种模式的主要思想是将数据的写入操作和查询操作分开。
它源于 Bertrand Mayer 设计的命令查询分离 (CQS) 原理。CQS 声明一个类只能有两种方法:改变状态并返回 void 的方法和返回状态的方法。而 Greg Young 是负责命名这种模式为 CQRS 并推广它的人。
首先来看看在没有 CQRS 之前是如何处理系统中的修改和查询的吧,如图 1 所示:
图 1 传统的系统请求
传统的系统请求从最左边的 Client 开始,沿着红线往右通过 Application Service 对系统进行请求。这里 Application Service 可以理解为系统的门面,或者是 Controller 层负责接收客户端的请求,此时请求的内容比较简单基本和数据库中的信息一致,因此这里使用 DTO(Data Transfer Object)直接请求。DTO 经过 Domain Model 以后直接到达 Database,从而沿着蓝色的线条返回给 Client 端。传统的请求方式部分读操作和写操作,都使用同样的数据模型和一套 Domain Model 以及相同的数据库。
从传统操作来看 Client 的请求在经过 Application Service,用户意图全部被分解为 CRUD 操作,但是在 Domain Model 中是无法体现的。为保证 DTO 的完整性和一致性,与操作无关的信息会被纳入 DTO,查询操作和创建操作都共用一个 DTO,而领域模型的业务流程被弱化。为了适应同时适应查询和创建操作,DTO 被设计的面面俱到,也就显得臃肿。从而在传输中存在不必要的字段传递。
而且一次操作,在 DTO 与领域对象间进行多次转换,增加了系统复杂度。还有,读写操作将围绕同一数据模型展开,对于读多写少的系统而言效率并不是最高的,特别在读操作为主的高并发系统中缺点就尤为突出。
正因为传统系统架构存在上面这些问题,因此 CQRS 根据读写职责的不同,把领域模型切分为 Command 端与 Query 端两个部分,如图 2 所示,红色线部分就是 Command 端,其对应的是 Domain Model 对其发送 Command 操作的指令往数据写入状态信息。
Query 端作为查询操作,由蓝色的线表示,通过 Query Model 向数据库获取信息,通过黑色向左的先返回结果给 Client。Command 端与 Query 端都通过 Application Service 进入系统,共享同一个数据库,但 Command 端只写入状态,Query 端只读取状态。
图 2 CQRS 分为 Command 端和 Query 端
目前而言已经将读写操作分开了,由于两个操作依旧共用一个数据库,为了提高读写效率数据库的分离就成为必然的选择。如图 3 所示,于是将原来的 Database,分离为 Writer Database 和 Reader Database 分别用于写操作和读操作。为了保证读写操作的数据一致性,需要在两个数据库之间进行数据同步。
由于数据同步是由时效性的,因此写入方是 Command 端,读取方是 Query 端,因此系统智能保证最终一致性。那么如何保证两个库之间的同步呢? 下面需要引入 Event Sourcing 的概念。
Event Sourcing 原理与应用
Event Sourcing 也叫事件溯源,是 Martin Fowler 提出的一种架构模式。其设计思想是系统中的业务都由事件驱动来完成。系统中记录的是一个个事件,由这些事件体现信息的状态。业务数据可以是事件产生的视图,不一定要保存到数据库中。
为了便于理解 Event Sourcing 我们通过一个例子来进一步解释,如图 3 所示:
图 3 Command 端和 Query 端 读写数据库的分离
我们从左往右看。对于一个业务类“账户”,拥有“属性”包括“账户 ID”和“账户金额”信息,同时拥有“方法”包括“创建账户”、“存现金”和“取现金”。中间绿色的事件序列,是针对“账户”进行的一些列操作,按照其中的序列号来看。
1. 创建了一个银行账户,假设此时的账户 ID 为“0001”。
2. 针对“0001”这个账户存入 300 元现金。
3. 然后从“0001”这个账户取出 100 元现金。
4. 最后,再存入 200 元。
上面生成的这一系列事件会保存到下方的 Event Store 的事件库中,这里并不会保存“账户”的状态信息。当需要获取“账户”数据的时候,会通过这些事件信息,还原成“账户”的最终状态,也就是“账户 ID”为“0001”,“账户金额”为 400。其具体实现方式是,通过账户相关的四个事件对应的处理方法,重新生成当前状态。如果每次查询状态信息都需要这样处理势必会造成资源的浪费,因此在右侧黄色的部分,我们将最终的“账户”信息通过视图的方式保存下来,以供查询。
图 3 Event Sourcing 实例图
上面这个“账户”处理的过程,就是 Event Sourcing,说白了就是通过事件的处理模式。它将系统中的操作都按照事件的方式记录并保存,任何实体的最终状态都是通过事件的叠加和还原确认的。
Event Sourcing 包含的内容
上面介绍了 Event Sourcing 的执行原理和基本概念,这里一起来看看其包含的主要内容,便于我们对它有更加全面的理解。
聚合对象:图 3 的例子中“账户”就是一个聚合对象,它里面包含“账户 ID”、“账户金额”等的基本信息,也包含了对账户操作的方法:“创建账户”、“存现金”、“取现金”。同时“账户”在领域驱动开发中对应的是一个领域模型。
Event Store:在 Event Sourcing 模式中,事件所保存的数据库称为 Event Store。在事件中需要包含聚合对象的 ID,以及事件的顺序。这样在查询的时候可以根据聚合 ID 从数据库中找到相关的事件,并通过事件的序号还原执行顺序。也就是事件的重现,也就是某一时刻执行的事件取出来,调用他的处理函数,还原那个时间点的业务状态。
为了获取最新的“账户”状态信息,需要通过 Event Sourcing 中获取对应的事件进行回放,从而获取当前的状态,这样的操作会浪费很多资源。因此我们会将聚合对象的最新数据状态,写到一个表中,这个表就是视图。又或者将这个状态信息发送给其他的应用程序进行后续的业务操作。
查询的内容是针对“账户”最终状态的,因此针对的对象应该是视图。这里的设定刚好的 CQRS 中的读写分离不谋而合,通过 Event Store 存放 Command 端的 Event 信息,通过视图存放实体最终状态的信息,而 Query 端从视图查询数据返回给用户。
Event Sourcing 的优缺点
上面介绍了 Event Sourcing 的原理和内容以后再来看看它的优缺点。
Event Sourcing 的优点:
溯源事件与重现操作:特别是在业务复杂的系统中,一个事务包含多个操作,它们有的是并行有的串行,如果需要了解操作的执行就需要对每个事件了如指掌。Event Sourcing 恰恰提供了事件的历史信息,方便查找任何时间点发生的事情。
追踪和修复 Bug:可以通过事件分析业务的执行过程,帮助发现 Bug,例如重方 Bug 产生时的事件序列,从而定位 Bug 所处位置。发现 Bug 并且修复以后,可以通过重新聚合业务数据,重放执行的事件序列验证修复结果,同时将 Bug 造成的损失进行挽回。
提高性能:Event Sourcing 模式下,由于是记录事件执行的序列,因此都是新增操作,没有更新操作,相对于需要更新操作的系统而言记录数据的性能是提高了。如果使用视图的方式将实体的最终状态可以传递给其他的应用,而不用写入数据库以后再读取,这种做法也提高了效率。
Event Sourcing 的缺点:
转变思路:Event Sourcing 的落地需要在设计时就用领域驱动的方式开展,需要有基于事件的响应式编程思维。这种方式需要以领域模型设计优先,而不是传统的数据库设计优先。
变更事件结构:随着业务流程的变化需要不断调整事件结构,对事件添加或者修改一些数据。这种行为会影响到“历史重现”,需要考虑兼容之前的事件结构。
处理幂等事件:如果对应的事务在执行过程中被中断,需要通过事件回放的方式达到事务的最终一致性问题。此时需要对事件的幂等性提出要求,也就是同一个事件运行多次得到的结果不变。需要在事件处理时丢弃重复事件。
查询事件数据库(event store):由于数据库中存放的一个个事件,如果针对实体状态的查询会相对困难。需要将这些事件重放,获取最新的实体状态的信息。这也是为什么需要通过 CQRS 的方式将读写进行分离,Command 端使用 Event Sourcing 而 Query 端使用 Event Sourcing 发出 Event 的最终状态进行查询的原因。
CQRS 与 Event Sourcing 的 完美结合
通过上面对 Event Sourcing 的介绍,可以发现它针对 Event 进行记录存放到 Event Store 中,并且把最终的状态放到视图中进行保存可以供给 Query 端进行查询。这种模式天生与 CQRS 就有默契的配合。
从 CQRS 模式的结构看,实体状态的变化发生在 Command 端,Command 端知道业务处理进行了哪些具体操作,将这些具体的操作进行封装就形成了 Event。
而 Query 端,查询返回的是实体当前状态状态。根据“当前状态 + 变化 = 新的状态”,如果能从 Command 端得到“变化”,再加上 Query 端自身获取的“当前状态”就能得到变化后的“新的状态”。
此时 Command 端发出的 Event 正好符合这个“变化”,如果当变化发生也就是新 Event 产生时,由 Command 端将这个 Event 推送到 Query 端,Query 端根据 Event 刷新状态,就能保证两端实体状态一致,达到最终一致性,如图 4 所示:
图 4 Event Sourcing 和 CQRS 结合
在图 3 的基础上加入 Event Handler 也就是图中蓝色部分,这部分接收从 Domain Model 中发过来的 Event 信息,也就是最新的实体修改信息。再将这个信息存放到 Reader Database(也可以理解为视图)中,这样新的 Event 信息加上当前的实体信息就时最新的实体信息了。而采用这种方式以后 Query 端依旧可以通过 Reader Database 获取数据对其原来的操作并没有产生影响。
再回到 Command 端,其对应的多次操作的 Event 会存放到 Event Store 中,作为业务跟踪的记录被保存下来。
上面提到的只是一种系统架构的模式,在实际运用中可以根据具体情况进行改进和优化。如图 5 所示,可以在 Command 端和 Query 端进行 Event 交换的时候加入队列,满足两套应用程序部署在不同进程的场景需求。
图 5 Command 端和 Query 端加入队列
一个 CQRS 的例子
上面聊到了 CQRS 与 Event Sourcing 的完美结合,这里通过一个例子给大家进一步介绍其运作的过程。这个例子的背景是,对于用户 (User) 而言保存了对应的联系方式(Contact) 和住址(Address)。
Command 用来建立 (Create) 用户 (User) 和更新(Update) 用户 (User);Query 用来查询用户(User) 对应的住址 (Address) 和联系方式(Contact)。
如图 4 所示,Client 请求应用分为上线两条线,分别用四种颜色代表。我们根据不同颜色来讲解 Command 端和 Query 端执行的过程。
图 4 Event Sourcing 和 CQRS 结合
红色向左的线:这里主要是针对 User 的 create 和 update 操作,分别填充 CreateUserCommand 类和 UpdateUserCommand 类,作为 UserAggregate 聚合类的输入参数。在 UserAggregate 中分别由,handleCreateUserCommand 和 handleUpdateUserCommand 两个方法处理,最后通过 UserWriteRepository 来保存到 Write database 中。
绿色向下的线:其连接了紫色的区域是 UserProjection,它的作用是将 Write database 的数据同步到 Read database 中。
蓝色向右的线:Client 发起 Query 请求通过 AddressByRegionQuery 类和 ContactByTypeQuery 类构建请求,将其传送到 UserProjection 类进行处理,其中 handle 方法分别对两类参数的请求进行处理。最后通过 UserReadRepository 获取 Read database 中的信息。
紫色向左的线:当从 Read database 中获取信息以后,返回给 Client。
图 6 CQRS 例子图解
在了解了整体架构以后再来看看具体实现的类结构。
如图 7 所示,User 实体类包括如下几个字段,也就是我们要操作的业务实体。包括用户的基本信息,其中 contact 和 address 类的具体信息在这里不展开描述。
图 7 User 实体类
Command 的类信息如图 8 所示,其内容相对简单。针对 CreateUserCommand 主要用于创建用户,包括 UserID 和 FirstName 以及 LastName。
图 8 CreateUserCommand 类
如图 9 所示,UpdateUserCommand 中加入了地址和联系方式的更新内容。
图 9 UpdateUserCommand 类
有了 Command 再来看看聚合类 UserAggregate,由于其中包括 Create 和 Update 的处理方法,这里介绍其中的 handleCreateUserCommand 方法,也就是处理新建用户命令。
这里会创建一个 UserCreatedEvent 对象,并将其通过 WriteRepository 保存到 Write database 中。也就是在 ES 中的 Event store,同时会将 event 的 list 返回。
图 10 handleCreateUserCommand 类
在处理完 Command 以后会返回 Event,这个 Event 在保存到数据库中的同时,也会发送和 Query 端作为最新的实体状态进行更新,这里会用到 UserProjector 类完成映射。如图 11,所示,其中的 project 方法会针对 UserID 的 events 进行逐一处理。
图 11 UserProjector 类
看完了 Command 端和 同步的 Projector,再来看看 Query 端的类。如图 12 所示,AddressByRegionQuery 类定义了 UserID 和 State 信息。
图 12 AddressByRegionQuery 类
如图 13 所示,ContactByTypeQuery 定义了 UserID 和 ContactType 的信息。
图 13 ContactByTypeQuery 类
如图 14 所示,上面提到的 AddressByRegionQuery 和 ContactByTypeQuery 作为参数传入到 UserProjection 类的 handle 方法中,并且返回对应的 Contact 和 Address 信息。使用了 UserReadRepositiory 从 Read database 中获取数据。
图 14 UserProjection
最后,再来看看测试代码这里将其分为 7 个步骤,如图 15 所示。
随机生成用户 ID。
鸿蒙官方战略合作共建——HarmonyOS 技术社区
通过 CreateUserCommand,创建新建用户的 Command,并且通过 UserAggregate 生成对应的事件。
通过 UserProjector 将事件映射到 Query 端的数据库中。
通过 UpdateUserCommand,创建更新地址信息的 Command,生成对应的事件。
通过 UserProjector 将事件映射到 Query 端的数据库中。
通过 AddressByRegionQuery,创建查询地址信息的 Query。
执行查询从 Read database 中获取数据与假设值进行比较。
图 15 Command 和 Query 的执行过程
最后来看看这些文件的目录结构,如图 16 所示。
图 16 文件结构
到此,关于“DDD 里面的 CQRS 是什么”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注丸趣 TV 网站,丸趣 TV 小编会继续努力为大家带来更多实用的文章!