命令和查询责任分离(CQRS)是指将数据存储的读取和更新操作分开的一种模式。实施 CQRS 据称可以提高性能、可扩展性和安全性。迁移到 CQRS 模式所创造的灵活性,使系统能够随着时间的推移而更好地发展。不过 CQRS 模式有一些众所周知的陷阱,本文介绍了三种实用的场景。
CQRS 模式可以创造奇迹:它可以将可扩展性、性能、安全性最大化,甚至可以打破 CAP 定理 (1)。尽管如此,CQRS 还是因为其引入的复杂性而获得了一个有争议的名字。例如,Martin Fowler 在其 CQRS 文章 (2) 中认为,应该少用甚至谨慎地应用该模式。
- 对大多数系统来说,CQRS 增加了风险的复杂性
- 你应该非常谨慎地使用 CQRS
- 虽然 CQRS 是工具箱中的一种模式,但要注意的是,它很难用得好,如果处理不当,很容易搞坏你重要的部件。
从我的观点来看,CQRS 带来的复杂性在很大程度上是偶然的,也是可避免的。为了说明我的观点,我想先讨论一下 CQRS 的目标,然后分析一下基于CQRS 系统中常见的三个复杂性的来源。
CQRS 的目标
CQRS 的目标是使用多种模型来表示相同的数据,与可扩展性、可用性、安全性、性能都没有关系。在多个模型中表示相同的数据,这就是目标,剩下的都是副产品。不信?听听 Greg Young 在 DDDEU2016 大会上的演讲 (3) ,他说 CQRS 是为了支持 Event Sourcing (事件溯源)实现而发明的。而且大家可能都知道,Event Sourcing 模型对于写数据来说很强大,但是对于读数据来说却很糟糕,这就是当年他需要 CQRS 的原因:用多个模型来表示相同的数据。
CQRS 是如何实现这个目标的?通过确保只有一个模型作为数据的源头,所有的修改都通过这个模型来达到。
让我们来看看这个解读如何帮助我们解决一些复杂的问题。
复杂性陷阱一:单向命令,或者说过度的隔离
据我所知,所有的 CQRS 的定义都遵循这个模式。
- CQRS 是基于 CQS 原则,它指出,操作应该被分为两组:改变数据的命令和查询数据的命令。一旦我们将这一原则提升到架构层面,我们就会得到一个系统,用例被隔离成相同的两组:命令和查询。每个用例既可以是命令,也可以是查询,但绝对不能同时是命令和查询。
- 一旦用例被隔离,我们会得到很多好处:多种模型、不同的持久化机制、独立的可扩展性等。
你是否感觉到这里有什么问题?这个问题很微妙:所有的 CQRS 定义通常都是从解决方案 — 隔离开始,之后才定义问题 — 多模型。这就导致了对隔离太过热衷:甚至将命令定义为单向的,操作服务器只返回 Ack/Nack 响应,必须轮询一些读模型存储的实际命令才能返回结果。换句话说,复杂度如地狱式的释放。
解决办法:放宽隔离
让我们退一步,重新考虑一下隔离的问题。我们已经看到,根据 CQRS 的说法,为了在多个模型中表示相同的数据,一个用例既可以写数据,也可以读数据。读取模型不应该更新任何东西,这一点是不言而喻的,否则我们最终会有多个数据来源。但是,你真的应该让你的命令空转吗?
其实不然,在不违反任何原则的情况下,一个命令可以安全地返回以下数据。
- 执行结果:成功/失败。
- 如果失败:错误信息或验证错误。
- 如果成功:聚合的新版本号。
这些信息将极大地改善你的系统的用户体验,因为:
- 你不需要向外部来源查询命令执行结果,你马上就能得到它。在验证命令,以及返回错误信息方面变得非常简单;以及
- 如果你想刷新显示的数据,你可以使用聚合的新版本来确定视图模型是否反映了已执行的命令。不会再显示陈旧的数据了。
说到数据,我们能不能再放宽一点隔离?在很多情况下,受影响的聚合内部包含的任何数据都可以作为命令执行结果的一部分返回。但是,这里有一点细微的差别:确保返回的数据可以在以后从其中一个读取模型中查询。否则,在响应没有到达客户端的情况下,数据可能会有潜在的丢失风险。
你可以在 Daniel Whittaker 的博客 (4) 中看到这样一个例子,他在博客中讨论了命令执行对象用于验证命令的使用。
另外,在这个 gist 中 (5) ,你可以看到我在 C# 中使用的命令执行结果对象。
复杂性陷阱二:Event sourcing (事件溯源)
由于历史原因,CQRS 与 Event Sourcing 模式密切相关。毕竟,CQRS 的发明就是为了让 Event Sourcing 模式成为可能。但是,让我们重新评估一下这两种模式之间的耦合关系。
正如我之前所说,CQRS 的目标是允许在不同的模型中表示相同的数据。如果你正在使用 event source 域模型,你绝对需要 CQRS 来执行查询。然而,还有很多其他合理的理由来实现 CQRS,这些理由与 event source 无关。
- 你的系统以不同的表示模型显示其实体。
- 你必须支持不同的查询模型(搜索、图、文档等)。
- 写入和读入之间的差异很大,你希望将它们独立扩展。
- 你不喜欢 ORM。
这是否意味着在所有这些情况下,你必须走 Event Sourcing 路线?如果你这么做,你就深陷复杂度陷阱。Event source 是一种业务领域的建模方式,也可能是最复杂的方式。因此,只有当你的业务领域证明你的业务领域是合理的,你才应该采用 Event Sourcing。让我们来看看如何在其他情况下实现 CQRS。
解决方案:CQRS != Event Sourcing
我们已经学会了通过编写事件处理程序来生成投影。如果没有事件,如何实现投影?还有一种方法可以实现投影,我称之为“基于状态的投影”。这个主题值得单独写一个帖子,但我将简单介绍三种实现“基于状态的投影”的方法。
1. "脏"标志
你可以通过设置 IsDirty 标志来标记一个被更新的实体,并实现一个投影引擎来查询脏实例,并将更新的数据投影到不同的模型中。要重建投影,你只需要将所有记录的 dirty 标志设置就可以了。
2. 追加
在关系型数据库中,你可以在表层跟踪提交。例如在 SQL Server 中,你有一个内置的机制,即 "rowversion" 列。这样的功能也可以在其他关系型数据库中实现。投射引擎将以类似于补订的方式查询更新的行,并将更新的数据进行投影。要从头开始重建一个投影,必须将上次已知的提交 id “回滚” 到 0。
3. 数据库视图
如果你使用的是关系型数据库,而你需要的只是用不同的模型来表示它的数据,那么数据库视图就很好用。没错,一个完全有效的 CQRS 系统可以在数据库中实现。这可能是最不性感的解决方案 — 但它不仅可以工作,还自然地遵循了 CQRS 模式。
这些投影模型的方法可能并不酷,也不性感,但它们是有效的。我见过不少采用了这些方法的项目,它们的效果很好,没有无端地淹没在 event source 相关的复杂性中。
等等,我刚才是不是建议不惜一切代价忽略 Event Sourcing,因为它很复杂?当然不是! Event Sourcing 是你工具箱中最重要的工具之一。但是,作为任何工具,请在其上下文中使用它 — 能带来商业价值的业务领域。核心子领域。另一方面,通用子域和支持子域,这些子域足够简单,可以用事务脚本或活动记录模式实现,但仍然可以从 CQRS 中受益。在这种情况下,使用最简单的工具来完成工作,并使用基于状态的预测来获取 CQRS 的好处。
复杂性陷阱三:好东西太多了
微服务的炒作吸引了很多人对 CQRS 的关注:如果你有一组独立的服务需要查询彼此的数据,那么 CQRS 就是通用的解决方案 (6) 。然而我已经看到这种方法产生了巨大的数据流图,在服务之间投射出大量的数据。
这不一定是坏事,但在很多情况下这可能是一个信号,需要退一步重新考虑你的分解策略。有可能是你的服务过于细化,没有反映出业务领域的边界。如果是这种情况,你可以通过将服务边界与相应的业务域重新对齐,大大降低架构的复杂性。
CQRS:解构
我想用 CQRS 的图来总结一下。
此图与您在网上可以找到的其他图表不同。
这就是我所看到的和实现 CQRS 模式。命令有响应。定义的投射机制是抽象的,与实现细节无关。里面可能是基于事件,或者是基于状态,甚至是数据库视图。最后,没有事件源(Event Sourcing)。按照业务域的要求,对系统的业务逻辑进行建模:活动记录、域模型或事件源域模型。
与每一个正确应用的工具一样,CQRS 应该降低复杂性,而不是诱导复杂性。如果你的体系结构的复杂性增加了,那么你很可能做错了。
文中相关链接:
http://codebetter.com/gregyoung/2010/02/20/cqrs-and-cap-theorem/
https://martinfowler.com/bliki/CQRS.html
https://youtu.be/LDW0QWie21s?t=448
http://danielwhittaker.me/2016/04/20/how-to-validate-commands-in-a-cqrs-application/
https://gist.github.com/vladikk/86da55d0eb09d7a291b9f9a5b406f2c9
https://www.ibm.com/developerworks/cloud/library/cl-build-app-using-microservices-and-cqrs-trs/
英文原文:
https://vladikk.com/2017/03/20/tackling-complexity-in-cqrs/
本文转载自微信公众号「高可用架构 」,可以通过以下二维码关注。转载本文请联系高可用架构 公众号。