解决CQRS中的复杂问题


CQRS模式可以创造奇迹:它可以最大化可扩展性,性能,安全性,甚至“击败”CAP定理。尽管如此,CQRS因其引入的复杂性而获得了一个有争议的名称。Martin Fowler认为应该谨慎地应用这种模式,甚至是谨慎应用:

  • “...对于大多数系统而言,CQRS增加了风险的复杂性”
  • “......你应该对使用CQRS非常谨慎”
  • “因此,虽然CQRS是一种在工具箱中很好用的模式,但要注意它很难很好地使用,如果你处理不当,你可以很容易地切掉重要的部分。”

从我的观点来看,CQRS引起的复杂性在很大程度上是偶然的,因此可以避免。为了说明我的观点,我想讨论CQRS的目标,然后分析基于CQRS的系统中3个常见复杂性的常见来源。

CQRS的目标
CQRS的目标是使用多个模型实现相同数据的表示。无关可扩展性,无关可用性,无关安全性,无关性能,多个模型中的相同数据而已,其余的优点是副作用。
听听Greg Young在DDDEU2016会议上的演讲,他说CQRS是为了支持Event Sourcing的实施而发明的。而且,正如您可能知道的那样,事件溯源模型对于编写数据非常棒,但读取却很糟糕。这就是他当时需要CQRS的原因:在多个模型中表示相同的数据。
CQRS如何实现这一目标?通过确保只有一个模型作为事实的来源,并且所有更改仅仅针对此模型。
让我们看看这种理解如何帮助我们解决一些复杂问题。

复杂性陷阱#1:单向命令,或过度分离
我所知道的所有CQRS定义都遵循这种模式:

  • CQRS基于CQS原则,该原则指出操作应分为两组:更改数据的命令和返回数据的查询。一旦我们将这个原则提升到架构层面,我们就会得到一个将用例分为两组的系统:命令和查询。每个用例可以是命令或查询,但不能同时使用。
  • 一旦用例被隔离,我们就会获得很多好处:多个模型,不同的持久性机制,独立的可伸缩性等。

你觉得这里有什么不对吗?问题很微妙:CQRS的所有定义通常都是从解决方案开始 - 隔离,然后才定义问题 - 多个模型。这引起了对隔离的过多热情:直到将命令定义为单向,需要从对方服务器获得请求/响应,这样就必须轮询一些读模型存储数据库才能获得实际的命令执行结果。换句话说,复杂的地狱释放出来了。

解决方案:放松隔离
让我们退后一步,重新考虑隔离。根据CQRS,我们已经看到,在多个模型中表示相同的数据,用例可以写入或读取数据;读模型不应该更新任何内容,这是一个明智的选择,否则我们最终会得到多个真实来源。但是你真的应该让你的命令无返回吗?
并不是的。在不违反任何原则的情况下,命令可以安全地返回以下数据:

  • 执行结果 - 成功/失败;
  • 发生故障时出现错误消息或验证错误;
  • 聚合的新版本号,如果成功;

此信息将显着改善系统的用户体验,因为:
  • 您不必轮询外部源以获取命令执行结果,您可以立即使用它。验证命令和返回错误消息变得微不足道; 
  • 如果要刷新显示的数据,可以使用聚合的新版本来确定视图模型是否反映执行的命令。不再显示陈旧数据。

说到数据,我们可以放松一点隔离吗?在许多情况下,包含的任何数据内受影响的集合可以被返回作为命令执行结果的一部分。但是,这里有一点细微差别:确保稍后可以从其中一个读取模型查询返回的数据。否则,如果响应未到达客户端,则存在数据可能丢失的轻微风险。

复杂性陷阱#2:事件溯源
由于历史原因,CQRS与事件溯源模式密切相关。毕竟,CQRS的发明是为了使事件采购成为可能。但让我们重新评估两种模式之间的耦合。
正如我之前所说,CQRS的目标是允许在不同模型中表示相同的数据。如果您正在使用事件源域模型,那么您绝对需要CQRS才能执行查询。但是,有许多其他完全正确的理由来实施与事件采购无关的CQRS:

  • 您的系统应以不同的表示模型显示其实体。
  • 您必须支持不同的查询模型(搜索,图形,文档等)。
  • 写入和读取之间的差异很大,您希望独立扩展它们。
  • 你讨厌ORM。

这是否意味着在所有这些情况下你必须沿着事件溯源路线前进?如果你这样做,你就会陷入复杂陷阱。事件溯源是一种建模业务领域的方式。不仅仅是一种方式,而且可能是最复杂的方式。因此,当且仅当您的业务领域证明其合理时,您才应该使用事件源。让我们看看如何在其他情况下实现CQRS。

解决方案:CQRS!=事件采购
我们已经被教导通过为事件编写处理程序来生成预测。但是如何在没有事件的情况下实施投影?还有另一种做投影的方法,我将其称为“基于状态的投影”。这个主题值得一提,但我将简要描述实现“基于状态的预测”的三种方法:

1. Is Dirty标志
可以通过引发IsDirty标志来标记已更新的实体,并实现一个投影引擎,该引擎将查询脏实例并将更新的数据投影到不同的模型中。要重建投影,您只需要为所有记录重新标记。

2.Catch-Up
在关系数据库中,您可以在表级别跟踪提交。例如,在SQL Server中,您有一个内置机制,即“rowversion”列。这种功能也可以用于其他关系数据库。投影引擎将以类似追赶订阅的方式查询更新的行,并投影更新的数据。要从头开始重建投影,您必须将最后一个已知的提交ID“回滚”回0。

3.数据库视图
如果您使用关系数据库,并且您只需要在不同的模型中表示其数据,那么数据库视图将非常有用。是的,可以在数据库中实现完全有效的CQRS系统。可能是最不性感的解决方案 - 但它不仅有效,而且自然也遵循CQRS模式。
这些投射模型的方式可能并不酷和性感,但它们有效。我已经看过很多项目都采用了它们,它们就像一个魅力,没有不合理地淹没在事件溯源相关的复杂性。

等等,我是否建议不惜一切代价忽略事件溯源,因为它很复杂?一定不行!事件溯源是您工具箱中最重要的工具之一。但作为任何工具,在其上下文中使用它 - 带来商业价值的业务领域:核心子域。另一方面,通用和支持子域很简单,可以使用事务脚本或活动记录模式实现,但仍然可以从CQRS中受益。在这种情况下,使用最简单的工具来完成工作,并使用基于状态的投影来消除CQRS的好处。

复杂性陷阱#3:太多好事
微服务炒作引起了很多人对CQRS的关注:如果你有一套需要查询彼此数据的独立服务,那么CQRS就是常见的解决方案