32 CQRS(下):CQRS还有哪些变化?
你好,我是钟敬。
上节课,我们从业务需求出发,一步步推演,学习了CQRS的基本原理。另外,我们还学习了“代码结构分离”和“数据库结构分离”两种策略。
在这两种策略中,程序仍然在同一个微服务,数据库也只有一个实例。但是,当我们遇到更高的并发性能需求时,就要考虑分布式程序和数据库了。这就是今天的两种策略要解决的问题。之后,我们还会讨论如何权衡这些策略,做出恰当的技术决策。
应用服务分离
我们先回顾一下 上节课 里,“数据库结构分离”部分的架构图。
如果我们发现,使用命令的并发请求相对比较少,而使用查询的并发请求却很多,需要横向扩展才能满足性能和可用性要求,那么就可以考虑拆成两个微服务了。我们把这种策略称为“应用服务分离”。
拆分后的架构图是后面这样。
对照这张图我们可以看到,命令处理器和查询处理器原来是在同一个微服务中的,现在拆成了两个。这样,两个服务的可伸缩性就可以不一样了。例如负责处理命令的微服务可以部署在 5 个容器里,而负责处理查询的微服务部署在 10 个容器里。
另外还有一个小细节,你可以结合后面这张图看一下。
在之前的组件图里,供给接口和需求接口是直接相连的。而这里使用了表示依赖的线。这两种方法都可以。如果供给和需求接口离得比较远,或者像这张图一样,两个需求接口共用一个供给接口,就可以采用依赖的形式来描述。
应用服务分离策略的好处是容易横向扩展,代价则是微服务的数量增加了,相应的运维和治理成本也就随之增加了。
数据库实例分离
在上面的例子里,微服务分开了,但是数据库实例并没有分开。虽然通过微服务的横向扩展,可以解决由于应用程序造成的性能瓶颈,但是如果性能瓶颈是由数据库引发的,那么拆分微服务的策略就无法解决问题了。这时候,就可以考虑“数据库实例分离”的策略了。
数据库实例分离的架构图是后面这样。
采用这种策略时,我们把数据库实例也分成了两个,分别用于命令和查询两种数据模型。
数据库之间用“变更数据捕获”(change data capture, 简称 CDC)机制来同步。目前有多种开源或商业的方案可以选择。多数方案的原理都是由命令模型的数据库日志的变化来触发,然后同步到查询模型的数据库。
除了通过数据库底层机制来同步数据以外,我们还可以在应用程序层面同步数据。
上面的架构图中,我们又使用了一个自定义的衍型 <
数据库实例分离以后,查询模型库就未必是关系型数据库了,我们可以选择 NoSql 数据库、内存数据库或其他数据存储技术。
数据库实例分离策略的好处是查询数据库可以横向扩展,可以灵活地选择数据存储机制,比较容易解决数据库造成的性能瓶颈。代价是增加了数据同步的复杂性,数据同步的延迟也可能会更高。
各种策略的组合
到现在,我们学习了 CQRS 四个层面的策略,分别是“代码结构分离”“数据库结构分离”“应用服务分离”和“数据库实例分离”。其中,“代码结构分离”是其他几种策略的基础,一般来说对于 CQRS 是必选的。
而其他三种策略都是可选的。而且,虽然咱们讲课的时候是按照层层递进的方式讲的,但其实这三种策略之间并没有依赖关系,你可以根据情况灵活组合使用。你可以先想想,一共有多少种组合,各自的优缺点是什么。
由于一共三种可选策略,每种都有使用和不使用两种选择,所以一共就可以有 8 种组合。我帮你梳理了一张策略选择表,列出了 8 种组合的主要优缺点。
为了实现命令进行的查询
最后,我再说一个微妙的问题。前面说命令和查询分离,给你的感觉可能是所有的数据库查询,都要绕过领域模型吧?
其实,有一种查询一般来说不会绕过领域模型,这种查询就是“为了实现命令而进行的查询”。比如说,修改员工信息,相应的应用服务可以经过后面这四个步骤。
第一步,把这个员工聚合从数据库里查询出来。
第二步,进行校验,看看是否符合修改的条件。
第三步,在内存中对员工聚合进行修改。
第四步,把修改的员工聚合存回数据库。
这里面的第一步,就是为了实现命令而进行的查询。这种查询,一般仍然要在命令处理器内部做,而不是在查询处理器中完成。
这是因为,这种查询的目的就是查出领域对象,然后进一步执行领域逻辑。而查询模型中的表结构可能已经按照查询的要求进行了反规范化,返回结果也是DTO而不是领域对象,这时候,转换回领域模型反而会更麻烦了。
这个问题,我们也可以从另一个角度来理解。CQRS 中的 Q(查询),指的其实是来自客户端的意图。也就是说,客户端的目的就是查询,才算是 CQRS 里的 Q,如果客户端的目的是增、删、改,在这个过程中发生的查询,一般不算是 CQRS 里的 Q。
总结
好,这节课的主要内容就讲完了,下面来总结一下。
今天我们学习了 CQRS 的另外两种策略,“应用服务分离”和“数据库实例分离”。我们还需要了解每种策略的优点和代价,这样才能结合项目实际情况,选择适合的策略。
对于 CQRS 来说,“代码结构分离”是必选的,其他几种则是可选的,并且相对独立,可以根据情况组合使用。 我们用表格总结了 8 种组合的主要优缺点。
另外,我们还讨论了“为了实现命令而进行的查询”其实不算 CQRS 里的 Q,而是应该在命令处理器中处理。不要小看了这个问题,虽然只是个细节,但也给不少小伙伴的实践带来过困扰。
学习了CQRS这两节课以后,不知道你的收获如何?之后,如果再有人对 CQRS 有不同意见,你就不要和他笼统地谈,而是问他,他所理解的是哪个层面的 CQRS。然后,根据他实际所指的含义来进行讨论,这样就比较容易达成一致了。
下节课,我们来聊聊怎样用分析模式解决更加复杂的建模问题,敬请期待。
思考题
最后给你留两道思考题。
1.其实我们之前还提出了另外两个需求。一个需求是给定一个时间段,可以看到某员工在这个时期内各个工时项的累计工时信息;另一个需求是,给定一个项目,可以看到这个项目下各成员的累计工时信息。你觉得可以有哪些方法实现这两个需求呢?
2.你以前是否用过 CDC 工具,可否分享一下你的使用经验,比较一下不同 CDC 工具的特点?
好,今天的课程结束了,有什么问题欢迎你在评论区留言,下节课再见。