Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说CQRS详解,希望能够帮助你!!!。
随着业务不断发展,软件系统的架构也越来越复杂,但无论多复杂的业务最终在系统中实现的时候,无非是读写操作。在大型系统中往往查询操作远远多于写入操作,于是就有了读写分离的思想,将读操作和写操作的模型分开定义并且提供不同的通道供用户使用
CQRS的全称是Command Query Responsibility Segregation
- 命令(Command):
不返回任何结果(void),但会改变对象的状态。命令操作会直接修改数据库,并针对多个领域模型的情况下我们需要增加来保证操作的原子性。而对于一个命令操作,我们往往是不直接依赖命令的返回值的,所以通常可以异步执行命令操作。对于一般系统来说,往往命令操作的使用频次会较低。
- 查询(Query):
返回结果,但是不会改变对象的状态,对系统没有副作用。查询操作并不会修改数据库中的内容,所以查询本身是一种幂等操作,以同一个查询条件在系统不改变的情况下反复执行会返回相同的结果,我们可以针对这种特性提供数据缓存来提高系统性能;同时因为不影响数据库,查询逻辑是不会产生数据一致性问题。查询往往会存在较高的使用频率。
逻辑是用控制器的动作方法编写的;就像我们有一个简单的电子商务应用程序,其中用户应该会下订单。我们有一个控制器,OrderController,用来管理订单。当用户下订单时,我们应该在数据库中保存记录。将很多业务方法写在控制器中
我们可以称之为“臃肿控制器”
MediatR基于命令的体系结构允许我们发送命令来执行某些操作,并且我们有单独的命令处理程序,使关注点分离和提高单一职责
MediatR是开源的,.net中的中介模式实现,一种进程内消息传递机制。支持同步或异步形式进行请求/响应,命令,查询,通知和事件的消息传递,并通过c#泛型支持消息的智能调度。
- 适当地使用中介者模式可以避免类之间的过度耦合,使得各类之间可以相对独立地使用。
- 使用中介者模式可以将对象间一对多的关联转变为一对一的关联,使对象间的关系易于理解和维护。
- 使用中介者模式可以将对象的行为和协作进行抽象,能够比较灵活的处理对象间的相互作用。
逻辑变得复杂
MediatR允许我们通过让控制器Action向处理程序发送请求消息来将控制器与业务逻辑解耦。MediatR库支持两种类型的操作。
命令(预期输出结果)
事件(请求者不关心接下来发生了什么,不期待结果)
CQ两端数据库表共享,CQ两端只是在上层代码上分离。
命令应基于任务,而不是以数据为中心。
命令可以放置在队列上进行异步处理,而不是同步处理。
查询从不修改数据库。 查询返回的 DTO 不封装任何域知识。
这种方案可以满足代码逻辑上的分离维护,但由于是使用同一数据库表,所以无法根据CQ两种业务的特点单独进行模型设计。
数据库是主从模式的时候,主库负责写入、从库负责读取,完全匹配Command和Query
在代码分离的基础上,我们可以再将数据存储的模型进行物理分离,读取存储可以是写入存储的只读副本,使用多个只读副本可以提高查询性能;也可能为读取模型单独设计库表。单独对查询和更新进行模型设计可以减小设计和实现的难度。并且此时读取数据库可使用自己的已针对查询进行优化的数据架构。比如读数据库可以直接存储查询数据宽表从而避免进行join操作或者复杂的查询映射。甚至可以针对读取操作使用mongo或者es等nosql数据库对查询逻辑进行增强。
在分库的情况下,就要对两个库进行数据同步
假如A库,添加一个物品,添加成功,然后发出通知,让B库更新数据。
然而因为网络问题导致了,此次通知超时,或者丢失,那么,A库更新后
B库却没更新,那么A,B两库数据就不一致。
分离后的数据将存在在不同的数据库中,Q的数据由C端同步过来。通常,这是通过在每次更新数据库时使写入模型发布事件来实现的。 而说到数据同步则就有同步执行和异步执行两种方案
同步:导致性能降低,但是可以保证数据的强一致性。
异步:拥有较高的性能,但需要系统接受最终一致性的
为了保持数据的一致性,就需要引入事件总线
- 什么是事件总线?
事件总线(EventBus)是一种机制,它允许不同的组件彼此通信而不彼此了解。 组件可以将事件发送到Eventbus,而无需知道是谁来接听或有多少其他人来接听。组件也可以侦听Eventbus上的事件,而无需知道谁发送了事件。 这样,组件可以相互通信而无需相互依赖。同样,很容易替换一个组件,只要新组件了解正在发送和接收的事件,其他组件就永远不会知道.
使用事件总线的目的:将微服务系统各组件之间进行解耦。
- CAP 是一个在分布式系统中(SOA,MicroService)实现事件总线(EventBus)和 最终一致性(分布式事务)的一个开源的 C# 库,她具有轻量级,高性能,易使用等特点。
- CAP 具有 Event Bus 的所有功能,并且CAP提供了更加简化的方式来处理EventBus中的发布/订阅。
- CAP 具有消息持久化的功能,也就是当你的服务进行重启或者宕机时,她可以保证消息的可靠性。
- 优势
相对于直接集成消息队列,异步消息传递最强大的优势之一是可靠性,系统的一个部分中的故障不会传播,也不会导致整个系统崩溃。 在 CAP 内部会将消息进行存储,以保证消息的可靠性,并配合重试等策略以达到各个服务之间的数据最终一致性。
代码正确的话,通过重试和持久化,就能解决网络原因带来的问题,保证消息正确传达
- Cap框架结构图
剖析:客户端调用微服务1→在本地事务中执行相关业务+发送消息存储到publish表中 →通过CAP框架开启新线程→CAP框架把消息发送到MQ中→MQ主动通过CAP框架调用微服务2→微服务2接收到消息并且本地业务执行成功,反馈ACK消息确认→MQ标记/删除消息。
整套流程涉及到4个角色:发布者、消息队列、订阅者、存储器。
CQRS的流程核心以事件为驱动,来进行主从数据库的读写分离,保持数据的一致,同时
记录事件发生的过程,以便在出现异常时,来对故障进行排查,同时能对业务进行复现。
- Event Sourcing也叫事件溯源,是Martin Fowler提出的一种架构模式。其设计思想是系统中的业务都由事件驱动来完成。系统中记录的是一个个事件,由这些事件体现信息的状态。业务数据可以是事件产生的视图,不一定要保存到数据库中。
2.上面这个“账户”处理的过程,就是Event Sourcing,说白了就是通过事件的处理模式。它将系统中的操作都按照事件的方式记录并保存,任何实体的最终状态都是通过事件的叠加和还原确认的
3.为了获取最新的“账户”状态信息,需要通过Event Sourcing 中获取对应的事件进行回放,从而获取当前的状态,也就是每次查询都要从头执行这四个事件。因此我们会将聚合对象的最新数据状态,写到一个表中,这个表就是视图。又或者将这个状态信息发送给其他的应用程序进行后续的业务操作。
- 查询的内容是针对“账户”最终状态的,因此针对的对象应该是视图,这里的设定刚好的CQRS中的读写分离不谋而合,通过Event Store存放Command 端的Event 信息,通过视图存放实体最终状态的信息,而Query 端从视图查询数据返回给用户。
就是在把事件执行一遍后,讲最新状态记录下来,下次自己返回最新状态
- Event Sourcing也叫事件溯源,是Martin Fowler提出的一种架构模式。其设计思想是系统中的业务都由事件驱动来完成。系统中记录的是一个个事件,由这些事件体现信息的状态。业务数据可以是事件产生的视图,不一定要保存到数据库中。
- Event Store:在Event Sourcing模式中,事件所保存的数据库称为Event Store
- 在Event Sourcing模式中,事件所保存的数据库称为Event Store。在事件中需要包含聚合对象的ID,以及事件的顺序。这样在查询的时候可以根据聚合ID从数据库中找到相关的事件,并通过事件的序号还原执行顺序。也
- 获取最新的“账户”状态信息我们会将聚合对象的最新数据状态,写到一个表中,这个表就是视图。又或者将这个状态信息发送给其他的应用程序进行后续的业务操作。
- 查询的内容是针对“账户”最终状态的,因此针对的对象应该是视图。这里的设定刚好的CQRS中的读写分离不谋而合,通过Event Store存放Command 端的Event 信息,通过视图存放实体最终状态的信息,而Query 端从视图查询数据返回给用户。
- 溯源事件与重现操作
Event Sourcing 恰恰提供了事件的历史信息,方便查找任何时间点发生的事情- 追踪和修复Bug
可以通过重新聚合业务数据,重放执行的事件序列验证修复结果- 提高性能
记录事件执行的序列,因此都是新增操作,没有更新操作
- 变更事件结构
随着业务流程的变化需要不断调整事件结构,需要考虑兼容之前的事件结构。- 处理幂等事件
事务在执行过程中被中断需要通过事件回放的方式达到事务的最终一致性问题- 由于数据库中存放的一个个事件,如果针对实体状态的查询会相对困难
这也是为什么需要通过CQRS的方式将读写进行分离
Command端使用Event Sourcing 而Query端使用Event Sourcing 发出Event 的最终状态进行
- 通过上面对Event Sourcing 的介绍,可以发现它针对Event 进行记录存放到Event Store中,并且把最终的状态放到视图中进行保存可以供给Query端进行查询。这种模式天生与CQRS就有默契的配合。
- 从CQRS模式的结构看,实体状态的变化发生在Command端,Command端知道业务处理进行了哪些具体操作,将这些具体的操作进行封装就形成了Event。
3.而Query端,查询返回的是实体当前状态状态。根据“当前状态 + 变化 = 新的状态”,如果能从Command端得到“变化”,再加上Query端自身获取的“当前状态”就能得到变化后的“新的状态”。
举例总结整个事件的过程
简单理解:
- 路人甲干(主数据库(写Command))
一天干了什么事(Event)
都通过相机(EventStore)记下来,
并通过QQ(事件总线)
将最新行程发给炮灰乙(从数据库(读Query))
炮灰乙就能通过QQ来复盘他一天在干嘛(溯源)
并通过QQ炮灰乙知道路人甲现在,在什么状态(数据最终一致性)
并对他一天什么做的不对,进行纠正(异常修正)
- 举例:比如路人甲一天事件的全记录:(举例并非实际物品,只是类举)
3.1 出门—>影像记录—>QQ---->炮灰乙接收成功—>看完知道了路人甲的最新状态 —>炮灰乙说收到且看完了
3.2 吃早饭—>影像记录—>QQ---->网络延迟,没接收—>QQ自动重发–>网络恢复–>看完知道了路人甲的最新状态 —>炮灰乙说收到且看完了
3.3 坐公交—>影像记录—>QQ---->炮灰乙网络断了,没接收—>QQ自动重发–>一直没接收—>QQ不再重试,并报错说,炮灰乙出了问题,通知炮灰乙的家人(管理员)代为检查状况。
3.4 逛公园—>影像记录---->网络延迟,没发送成功–>网络恢复—>重发–>QQ---->炮灰乙接收成功—>看完知道了路人甲的最新状态 —>炮灰乙说收到且看完了
3.5 迷路—>影像记录---->网络延迟,没发送成功—>重发–>一直没成功---->检查手机状况,以及查看QQ服务是否挂掉
3.6 回家—>影像记录—>QQ---->炮灰乙接收成功—>看完知道了路人甲的最新状态 —>炮灰乙说收到且看完了- 最后这些记录下来的事件,在日后可以用来溯源,来回顾今天都做了什么,
4.在接送到迷路的记录后,可以看懂这个错误,从而可以进行总结,通过地图梳理,来进行错误纠正,在下次就不会迷路了
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
上一篇
已是最后文章
下一篇
已是最新文章