什么是CQRS ?

CQRS(Command Query Responsibility Segregation),命令查询职责分离。它是一种软件架构模式,核心思想是将应用程序写入(命令)和查询(Query)分离。


核心概念

命令端(写)

负责处理业务逻辑、数据修改,通常是写入关系型数据库

查询端(读)

负责处理数据读取逻辑,通常是读取经过优化后的数据,提供高性能的查询。


查询端读取经过优化后得到数据

唯一目标就是尽可能高效、快速地满足各种查询请求。

结构上的优化:非规范化与扁平化

传统的数据库设计为了减少数据冗余和保证数据一致性,通常会采用规范化(Normalization)的范式。这意味着数据会被分解到多张关联的表中。

读模型则倾向于采用非规范化(Denormalization)和扁平化的结构。 它的目标是:

  • 减少 Join 操作:规范化的数据库在查询时往往需要大量的 JOIN 操作来组合来自不同表的数据。JOIN 操作是耗时的。读模型会把查询经常需要的数据预先组合好,存放在一张或几张“宽表”中,这样查询时可以直接从单张表获取数据,避免复杂的关联查询。
  • 适应特定查询模式:例如,如果你的业务经常需要查询用户的订单历史和订单详情,在读模型中可能就会有一张表直接包含 用户ID用户名订单ID订单创建日期商品名称商品数量商品价格 等所有信息,而不需要连接 用户表订单表订单商品关联表商品表
  • 预计算和聚合:一些需要复杂计算或聚合(如销售总额、商品库存)的报表数据,可以在数据写入或更新时预先计算好并存储在读模型中,避免在查询时进行昂贵的大数据量计算。

存储技术上的优化:选择最适合查询的数据库

写模型通常为了保证事务的 ACID 特性,会选择关系型数据库(如 MySQL、PostgreSQL)。

读模型则可以根据查询的特点选择最合适的数据库或存储技术,而不受写模型的限制:

  • 关系型数据库(优化后的结构):即便读模型仍使用关系型数据库,它的表结构也会是为查询而非写入设计的,可能包含物化视图 (Materialized Views) 或宽表。
  • 文档数据库 (Document Databases):如 MongoDB,非常适合存储非规范化的、类似 JSON 格式的数据。如果查询经常需要整个“文档”的数据,文档数据库能提供极高的读取性能。
  • 搜索引擎 (Search Engines):如 Elasticsearch,专门为全文搜索、模糊查询、聚合统计等场景优化。如果你的查询包含大量搜索功能,将读模型同步到 Elasticsearch 会极大提升性能。
  • 列式数据库 (Columnar Databases):如 ClickHouse、Apache Druid,非常适合聚合查询和分析报表,尤其当查询只关注少数列时,性能优势显著。
  • 内存数据库/缓存 (In-Memory Databases/Caches):如 Redis,对于需要极低延迟的热点数据查询非常有效。可以将读模型的某个视图加载到缓存中。
  • 图数据库 (Graph Databases):如果查询涉及复杂的实体关系遍历,如社交网络中的好友关系查询,图数据库会是更好的选择。

查询接口的优化:扁平化与多样化

写模型通常通过领域模型的接口进行操作,这些接口可能包含了复杂的业务逻辑和数据校验。

读模型则提供扁平的、直接的查询接口,以适应前端或外部系统的各种查询需求:

  • 简单的 CRUD 接口:查询端通常提供更直接的数据访问接口,可能只是简单地查询数据并将其原样返回。
  • 丰富的视图:可以为不同的用户角色或前端页面提供不同的“视图”,每个视图都是一个针对特定查询场景优化过的读模型实例。
  • GraphQL 或 OData:在读模型之上构建 GraphQL 或 OData API,允许客户端灵活地定制查询字段和关系。

数据更新策略:最终一致性与异步同步

写模型的数据更新是实时的,关注强一致性。

读模型的数据更新通常是异步的,通过监听写模型发布的事件来同步数据,因此它接受最终一致性。

  • 当写模型中的数据发生变化时,它会发布一个事件
  • 读模型会有一个或多个事件处理器/投影器 (Projector) 订阅这些事件。
  • 当事件发生时,投影器会读取事件内容,并以最适合读模型结构的方式更新读模型中的数据。这个过程是异步的,可能会有短暂的延迟,但最终会达到一致。

示例理解

想象一个电商系统:

  • 写模型(Command Side)
    • 数据库:关系型数据库(MySQL)。
    • 表结构:orders 表(订单基本信息)、order_items 表(订单商品明细)、products 表(商品信息)、customers 表(客户信息),它们之间通过外键关联,高度规范化。
    • 操作:创建订单需要插入多张表,并且有复杂的库存扣减、优惠券校验等业务逻辑。
  • 读模型(Query Side)
    • 场景 1:客户订单列表查询:可能在另一个数据库(或同一数据库但不同表)中有一张 customer_orders_view 的宽表,它包含了客户ID、订单ID、订单日期、总金额、商品数量、首个商品名称等所有客户订单列表需要显示的信息。当订单创建或更新时,会有一个事件处理器将最新数据同步到这张宽表。查询时直接从这张宽表查询,无需 JOIN
    • 场景 2:商品搜索:商品信息更新后,会发布 ProductUpdatedEvent。一个事件处理器将商品数据同步到 Elasticsearch。当用户在网站上搜索商品时,直接查询 Elasticsearch,实现毫秒级的全文搜索响应。
    • 场景 3:运营报表:可能将订单数据汇总后存入 ClickHouse,用于快速生成各种销售额、商品销量等分析报表。

在这些读模型中,我们不再关心如何插入数据、如何保证事务完整性(那是写模型的责任),我们只关心如何让查询变得最快、最方便。这就是“读模型通常是针对读取优化的”的含义。

解决什么问题?

读写分离

​ 解决了读写负载不均衡的问题,可以独立扩展读写服务。

性能优化

​ 查询端可以针对查询场景进行数据模型优化和存储优化,提供极高的查询性能。

数据一致性

​ 读写之间是最终一致性。

适用场景

读写分离明显

​ 读操作远多于写操作,且读写性能要求差异大。

复杂查询

​ 需要进行多维度、复杂的聚合查询,而写操作模型不适合直接查询。