CQRS(Command-Query Responsibility Segregation) 是一种模式,它告诉我们将数据的查询与数据的操作分开。
它源于 Bertrand Mayer 设计的命令查询分离(CQS)原理。CQS声明一个类只能有两种方法:改变状态并返回void的方法和返回状态但不改变它的方法。
Greg Young 是负责命名这种模式为 CQRS 并推广它的人。如果您在互联网上搜索CQRS,您会发现许多由Greg制作的优秀帖子和视频。例如,你可以找到在CQRS模式的优秀和非常简单的解释在 这个 帖子。
我们想要展示保险领域的例子 - PolicyService 。负责管理保险单的服务。以下是在应用CQRS之前具有接口的代码段。所有方法(写入和读取)都在一个类中。
<b>interface</b> PolicyService { <b>void</b> ConvertOfferToPolicy(ConvertOfferRequest convertReq); PolicyDetailsDto GetPolicy(<b>long</b> id); <b>void</b> AnnexPolicy(AnnexRequestDto annexReq); List SearchPolicies(PolicySearchFilter filter); <b>void</b> TerminatePolicy(TerminatePolicyRequest terminateReq); <b>void</b> ChangePayer(ChangePayerRequest req); List FindPoliciesToRenew(RenewFilter filter); }
如果我们在这种情况下使用CQRS模式,我们会得到两个独立的类,更好地满足 SRP 原则。
<b>interface</b> PolicyComandService { <b>void</b> ConvertOfferToPolicy(ConvertOfferRequest convertReq); <b>void</b> AnnexPolicy(AnnexRequestDto annexReq); <b>void</b> TerminatePolicy(TerminatePolicyRequest terminateReq); <b>void</b> ChangePayer(ChangePayerRequest req); } <b>interface</b> PolicyQueryService { PolicyDetailsDto GetPolicy(<b>long</b> id); List SearchPolicies(PolicySearchFilter filter); List FindPoliciesToRenew(RenewFilter filter); }
这是应用CQRS的第一步。什么是简单的转变会带来很大的后果并开辟新的可能性,我们将在本文的后面部分进行探讨。
CQRS能做什么?
大多数时候,改变状态所需的数据在形式或数量上都不同于用户需要查询所需的数据。使用相同的模型来一起处理查询和命令会会导致模型膨胀,只依靠一种类型来操作所需的所有东西,模型复杂性也会增加,聚合大小通常会更大。
CQRS使我们能够使用不同的模型来改变状态和不同的模型来支持查询 。通常写操作的频率低于读操作。 具有单独的模型和分离的数据库引擎允许我们独立地扩展查询端并更好地处理并发访问,因为读取端不再堵塞写入或命令端(在相反的情况下)。
使用单独的命令和查询模型,我们可以将这些职责分配给具有不同技能的不同团队。例如, 您可以为高技能的OOP开发人员分配命令端,而熟悉SQL开发人员可以实现查询端。 CQRS让您扩展您的团队,让您最好的开发人员专注于核心的东西。
CQRS是一个架构吗?
人们常常弄错了。 CQRS不是顶级/系统级架构。 架构的示例包括: 分层 , 端口和适配器 (六角形或六边形架构)。CQRS是您在服务/应用程序“内部”应用的模式,您只能将其应用于您的部分服务。(banq注:CQRS是一种服务模型,微服务的模型,也就是指导你怎么做微服务的)
实施示例
有许多方法可以实现CQRS。它们具有不同的后果,这种解决方案的复杂性和适用性取决于您的系统环境。如果您是拥有1.5亿用户的Netflix,您需要采用不同的方法,并且不同的解决方案适用于仅有数百名用户的典型企业应用。我们认为,特别是在处理现有(遗留)项目时,最好的方法是解决CQRS的演变问题。
我们从不使用CQRS的解决方案开始。
UI(通过控制器层)使用服务外观层,该层负责协调域模型执行的业务操作。模型存储在关系数据库中。在我们的示例中,有 PolicyService 类( Java , C# ),它负责处理与策略相关的所有业务方法。
我们使用一个模型进行读写。执行业务操作时,我们使用搜索功能也是通过相同类实现,这可能会导致您的域模型只具有搜索所需的属性,或者更糟糕的是,您可能会强制设计域模型以便更轻松地查询它。
在这个例子中,我们想要显示开发人员使用分离模型进行写入侧和读取侧的代码。
在该示例中,使用中介者模式如XXXHandler,中介的作用是确保将命令或查询传递给其处理程序。中介接收命令/查询,该命令/查询只不过是描述意图的消息,并将其传递给处理程序,然后处理程序负责调用领域模型执行预期的行为。因此,可以将此过程视为对服务层的调用 - 总线在其间进行消息的管道连接。在Java示例中,我们创建了 Bus类 ,它是此模式的实现。 Registry 负责将处理程序与命令/查询相关联。在C#示例中,我们使用 MediatR 库为我们完成所有这些。您可以在我们的 另一篇文章中 阅读有关MediatR的更多信息。
现在我们有了单独的命令和查询入口点,我们可以引入不同的模型来处理它。NoCQRS解决方案使用RDBMS和ORM - 企业应用程序中的典型堆栈。通过此改变,我们可以将域模型用作命令模型。这个模型得到了简化:一些关联仅用于不再需要的读取查询,一些字段不再需要。
在查询模型上,我们可以在数据库中定义视图并使用ORM映射它,或者,对于查询模型,我们可以停止使用重量级ORM并将其替换为普通的旧 JDBC模板 或Java中的 JOOQ 或在.NET中的 Dapper 。
如果要避免在数据库中定义视图的复杂查询,可以执行下一步,并使用旨在处理查询的表替换视图。这些表将具有简单的结构,数据映射为用户在屏幕上看到的内容以及用户需要搜索的内容。(banq注:专门为查询读取设计的数据表结构)。添加这种类型的表替代数据库视图消除了编写复杂查询的负担,并为扩展解决方案开辟了新的可能性,但它要求您以某种方式使您的域命令模型与查询模型表保持“同步”。
同步方式:
最佳实践:
命令模型和查询模型之间同步方法的选择取决于许多标准。即使使用数据库视图,您也可以获得很好的结果,因为您可以使用只读副本来扩展数据库,该副本仅用于查询您创建的视图。
具有单独的表简化了读取,因为您不必再编写复杂的SQL,但您必须自己编写用于更新查询模型的代码。
没有神奇的框架会为你做这件事。与给定命令模型部件相关的读取模型的数量也是决策因素。如果您有一个聚合的2-3个查询模型,您可以安全地调用命令处理程序中的所有更新程序。它不会影响性能,但是如果你有10个,那么你可以考虑在更新聚合的事务之外异步运行它。在这种情况下,您必须检查是否允许最终一致性。这比业务决策更具商业决策,必须与业务用户讨论。
拥有单独的查询表是将CQRS解决方案提升到新水平的一个很好的步骤。
如果您想了解更多信息,请查看我们的示例,使用 Java 或 C# 。
单独的存储引擎
在这种方法中,我们为查询模型和命令模型使用不同的存储引擎,例如:
每个命令处理程序都应该发出包含所发生事件 ,领域事件Event是一个命名对象,表示在指定对象中发生的某些更改。事件应提供有关在业务操作期间更改的数据的信息。事件是域的一部分。在我们的示例中,我们有一些关于保险政策的事件 - PolicyCreated,PolicyAnnexed,PolicyTerminated,PolicyAnnexCancelled ( Java示例 , C#示例 )。
在读取方面,我们创建了事件处理程序(方法在特定类型的事件进入时执行),它们负责事件的投影创建(banq注:把事件再执行一遍更改查询数据表,此为事件的投影)。这些事件处理程序对持久性读取模型( Java示例 , C#示例 )执行CRUD操作。
什么是投影? 投影是将事件流转换(或聚合)为数据表结构或数据库视图的过程 。投影是将事件流转换(或汇总)为结构表示。这可以称为许多名称:持久性读取模型,查询模型或视图。
通过这种方法,我们可以应用不同的工具来执行查询,并使用不同的工具来执 通过这种方式,我们可以实现更好的性能和可伸缩性,但却以复杂性为代价。在典型的业务系统中,系统中执行的绝大多数操作将使用读取侧/查询模型。该元素应该为更高的负载做好准备,它应该是可扩展的,并允许构建允许高级搜索的复杂查询。使用这种方法,我们将不得不处理最终的一致性,因为各种数据源之间的分布式事务是性能杀手,而大多数NoSQL数据库都不支持它。
CQRS与事件采购(CQRS-ES)
下一步是更改命令端以使用事件源。这个版本的架构非常类似于上面(当我们使用单独的存储引擎时)。
关键区别在于命令模型。我们使用Event Store作为持久存储,而不是RDBMS和ORM。我们不保存实际的对象状态,而是保存事件流。这种管理状态的模式被命名为 Event Sourcing 。
我们不是通过改变先前的状态来保持系统的当前状态,而是将事件(变化)附加到过去事件(变化)的顺序列表中。这样我们不仅可以了解系统的当前状态,还可以轻松跟踪我们是如何达到这种状态的。
下面的示例显示了基于 足球游戏比赛域的 不同状态管理方法。
上图显示了Game对象的传统状态管理。我们有关于比赛结果以及比赛开始/结束的信息。当然,我们可以在这里建模其他信息,例如得分目标列表,犯规犯规列表,角落列表。但是,您必须承认 - 足球比赛的领域理想地由一系列随时间发生的事件描述。
当使用Event Sourcing来管理Game对象的状态时,我们可以准确地重现整个比赛。我们有关于哪些事件影响了当前对象状态的信息。上图显示每个事件都反映在特定的类中。这就是Event Sourcing的神奇之处。
大多数文章中提到的主要事件溯源优势之一是您不会丢失任何信息。在传统模型中,每次更新都会删除以前的状态 之前的状态丢失了。您可以说,有像 Envers 这样的日志,备份和库,但它们并没有为您提供有关更改原因的明确信息。它们只显示数据已更改的内容,而不是原因。在事件源方法中,您可以在域中的业务事件之后为事件建模,因此它不仅显示数据更改,还显示更改原因。
下一个优点是,通过一系列事件保存域聚合可以极大地简化持久性模型。您不再需要设计表格和它之间的关系。您不再受ORM可以和不能映射的限制。在使用像Hibernate这样非常先进的解决方案时,我们发现了一些情况,当我们不得不从我们域中的某些设计概念中辞职时,因为很难或不可能映射到数据库。
有越来越多的解决方案支持使用Event Sourcing( EventStore , Streamstone , Marten , Axon , Eventuate )创建应用程序。在我们的示例中,我们使用从 Greg Young的示例 派生的内存事件存储( Java示例 , C#示例 )的自己实现。这不是生产就绪的实现。对于生产级解决方案,您应该应用更复杂的解决方案,如 EventStore 或 Axon 。
哪些系统值得使用事件采购?
我应该使用CQRS / ES框架吗?
如果您对CQRS / ES没有经验,则不应该从任何框架开始。从核心域开始,实现一些业务功能。 当您的业务开始工作时,请关注技术内容。在开始实现自己的事件存储或命令总线之前,请评估Event Store或Axon等可用选项。有很多事情需要考虑,还有许多陷阱(并发,错误处理,版本控制,模式迁移)。
总结
有两个阵营:一个说你应该总是使用CQRS / ES,另一个说你应该只使用你的解决方案的一部分,并且只有当你需要具有高性能/可用性/可扩展性系统的高度并发系统时。 您应该始终根据您的要求评估您的选择。
即使是最简单的CQRS形式也能在不增加复杂性的情况下为您提供良好的结果。例如,使用视图进行搜索而不是使用域模型可以简化事情。在我们的系统中,我们还发现很多地方添加专门的读取模型表并同步更新它们给了我们非常好的结果(比如摆脱20多个表连接4个联合的视图定义并用一个表替换它)。 只要允许最终的一致性,使用像ElasticSearch这样的专用搜索引擎也是一个安全的选择。
如果您选择使用不同的存储引擎,事件总线和其他技术组件,CQRS可能会产生非常复杂的技术解决方案。只有一些复杂的场景和可扩展性要求才能证明这种复杂性(如果你在Netflix规模上运行)。同时,您还可以使用简单的技术解决方案应用CQRS,并从此模式中受益 - 您不需要Kafka来执行CQRS。
我们为这篇博客文章准备了两个版本的demo,一个用于Java开发人员,第二个用于.NET开发人员。以下链接:
CQRS的利弊
优点:
缺点:
ES事件溯源利弊
优点:
缺点: