CQRS命令查询分离架构的多种形式实现 - Kapil


CQRS(命令查询职责分离)的核心有一个简单的目标:将读取和写入分离为单独的模型。这个简单的想法可以采用多种形式,具体取决于使用它的上下文以及所使用的实现选择。这篇文章试图分析 CQRS 的各种形状,以及所有这些形状如何支持解耦读写的中心思想。
世界变得复杂了。信息系统不再只是简单的 CRUD 风格的应用程序,并没有适用于所有类型操作的规范数据模型!
云、微服务、流媒体、反应式系统意味着现在需要以不同的方式考虑这种多样化、分布式和始终动态的数据来源和数据消费。
CQRS 是这种思维过程的结果之一。出于记录和读取目的而以不同方式查看数据,并承认这两种模型不需要相同。这种想法一旦被接受,它就会打开很多选项:

  • · 数据模型
  • · 数据存储
  • · 一致性选项
  • · 应用设计
  • · 运行时环境
  • · 访问模式(例如,大量读取,少量写入)

请注意,在 CQRS 一词被创造之前,它的原理通过数据仓库和分析产品的报告数据库仍在使用,这在某种程度上遵循了 CQRS 的核心主题,将读取和查询解耦。这些产品通常使用某种 ETL 或数据库复制过程从数据库中获取数据,并用作信息系统。虽然这些技术在一个层面上遵循 CQRS,但这个术语现在通常是指单个进程(例如,模块或微服务)为不同类型的用户请求采用读写模型。因此,它更像是一种 OLTP 模式,而不是以前的 OLAP 模式。
事实上,拥有分析系统和数据湖等可能是 CQRS 的因果优势,但这些都不是根本原因。
最后值得一提的一点是,CQRS 在领域驱动设计成为话题之后开始流行起来。命令和查询指的是 DDD 中的域事件,因此它不是简单的 WriteReadResponsibilitySeggretaion,而是随着这些术语的 DDD 版本而广为人知。
下一节尝试分析可用于实现 CQRS 的不同选项以及这些选项如何影响以下特性:数据模型、一致性、数据库选择、传输模型。这些选项的顺序没有意义,但可能它们从简单到相对复杂。
  
以应用为中心的 CQRS
CQRS 的简单实现形式,旨在创建不同的读写服务和模型,以在这些操作之间提供清晰的模块子边界。命令服务在数据库中执行写入。查询服务从同一个数据库中读取数据,但会生成不同的查询模型,例如用于在 UI 中显示。查询模型可以选择对这些数据进行转换和投影。
底层数据库保持不变,因此 DB 约束和模式在两者中仍然有效。
由于底层数据库相同,DB一致性强(*)。
  
数据库只读副本
只读副本提供不同的读取数据库,其中包含主写入数据库的只读副本。
由于数据库结构与写入数据库完全相同,因此服务可以在代码中使用不同的查询模型。
此模式用于从查询中卸载写入数据库,以平衡一个或多个读取实例之间的查询流量。
复制工具通常由IBM DB2Oracle等 DB 供应商提供。较新的云原生托管数据库提供只读副本作为托管服务,并负责只读副本的复制和高可用性。
在特殊情况下,数据库复制也可以调整为强一致性(又名复制因子)。在这种情况下,复制是为了支持强一致性模型(CAP 中的 C)。例如,AWS S3 提供了强大的先写后读一致性模型。Cassandra 提供了可配置的读取一致性级别设置。
  
事件发布
许多事件驱动模式补充了 CQRS 风格的应用程序。命令服务写入命令数据库。因此,任何改变域状态的域事件都是一个命令,并被写入优化了写入的数据库。对于先读后写的情况,也可以事务性地读取命令数据库,但这通常是在聚合的小上下文中。此外,命令服务还将命令发布到消息传递系统和具有规范事件结构的命令事件中。
任何服务都可以订阅此事件,在这种情况下,查询服务订阅会侦听该事件并构建针对读取进行优化的本地数据库。与 DB 复制相反,这种方法提供了以任何读取优化方式投射事件的自由。事实上,事件底层数据库的选择可以与命令数据库不同,从而实现多语言持久化。
要点:
  • 1. 查询端会看到最终一致的数据,因此需要在应用程序设计和用户体验中考虑到这一点。对于要用于写入命令 DB 中的数据,不应读取查询 DB。
  • 2.命令服务需要以某种方式管理数据库写入和事件发布事务(都可以或都回滚)。这可能意味着 XA 或 2PC 方法可能很脆弱。
  • 3. 在失败的情况下,事件可以通过多次重试来传递,因此可以多次到达订阅者(称为“至少一次”传递)。订阅者需要是幂等的并且只处理一次事件。消息系统增加了 DR 组件,例如持久队列或死信队列,以在订阅者或消息系统出现故障时保留消息。
  • 4. 根据命令服务的执行时间发布事件,因此没有顺序保证。这意味着在 T1 发生的域事件 D1 可以晚于在 T2 发出的另一个域事件 D2 到达订阅者。此外,在订阅者失败的情况下,当订阅者再次健康时,事件可能会乱序重新传递。必须解决乱序交付问题(尤其是在 JMS 和 AMQP 风格的消息传递中,而在 Kafka 或 Akka & Kinesis Streams 等事件流系统中,消费者可以控制他们为某个时间消耗的消息偏移量划分)。通常,这是通过将事件时间戳作为消息属性来解决的,该属性不同于消息中间件在向其发布事件时创建的时间戳标头。

 
4. CDC(变更数据捕获)
CDC 将命令数据库作为命令事件的来源。命令服务只需要事务性地提交到事务数据库,然后数据库事务日志成为事件触发器,免除了命令服务来管理数据库写入和事务性事件。
这里引入了额外的日志抓取组件,它通常是一个数据库原生组件,并“挂钩”到事件存储以发布时间顺序事件。
要点:
  • 1. 参考前面“事件发布” #1
  • 2. 参考前面“事件发布” #3。此外,由于命令数据库事务日志仍然具有所有更改,因此它提供了另一层以在失败时重试。
  • 3. 数据库事务日志是有序的,保证了消息的排序。

数据库供应商为流行的数据库提供了一个日志抓取系统。这些与 CDC 工具结合使用以启用源和目标连接器。两个流行的选择是DebeziumKafka Connect。Kafka Connect 提供对大量“源”和“接收”连接器的支持。
云数据库将此模式作为托管的云原生服务提供。例如AWS Dynamo Streams(由AWS Kinesis Data Stream支持)。
这种模式也在微服务事务发件箱模式事务日志拖尾模式中捕获。
AxonEventuate这样的框架为这种模式提供了支持。
  
5. 事件溯源和 CQRS
事件溯源是一种将应用程序设计为一系列领域事件的方法,这些事件以时间顺序存储在事件存储中。事件存储可以是一个事件平台,例如 Kafka 或 AWS Kinesis,也可以只是一个数据库。Event Store 是关于状态的“单一事实来源”。重要的一点是域没有“当前状态”。当前状态是派生状态,可以通过在聚合上重放事件来实现。事件溯源将更改的隐式审计日志作为一组域事件提供。另一方面,由于没有实体状态,因此无法在命令事件存储中查询实体。为了克服这个问题,CQRS 通常被视为事件溯源的补充模式,使实体的派生状态在单独的查询数据存储中可用。
也可以使用 CDC 模式实现 CQRS。如果事件存储是像 Kafka 这样的消息传递系统,事件存储将成为查询服务(以及任何其他服务,如分析、欺诈预防等)的订阅,以获取域命令事件流并创建读取优化模型用于查询。
如果命令服务需要读取一些实体数据怎么办?理想情况下,它不应该依赖于先前的状态,因为所有命令服务应该做的只是命令的追加日志(事件存储的选择,即消息传递与 DB 对这一点来说意义不大)。但是万一呢?命令服务也可以读取查询存储投影吗?答案是根据上下文而定的,可能是可以的,但这里有一个最终的一致性。只要命令处理不依赖于它需要读取的数据的强一致性保证,它应该可以读取。可能将其保留为边缘情况是谨慎的。
框架: Lightbend Akka & Axon 
 
缓存:读层缓存是一种通用选择,在所有选项中都可用,而不是CQR的必然结果。
一致性:在这里,数据一致性考虑是最重要的。只读数据、命令端读取和先读后写数据的情况需要不同的一致性保证。