什么是CQRS模式?它解决了什么问题?在什么场景下会考虑使用它?
什么是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,用于快速生成各种销售额、商品销量等分析报表。
- 场景 1:客户订单列表查询:可能在另一个数据库(或同一数据库但不同表)中有一张
在这些读模型中,我们不再关心如何插入数据、如何保证事务完整性(那是写模型的责任),我们只关心如何让查询变得最快、最方便。这就是“读模型通常是针对读取优化的”的含义。
解决什么问题?
读写分离
解决了读写负载不均衡的问题,可以独立扩展读写服务。
性能优化
查询端可以针对查询场景进行数据模型优化和存储优化,提供极高的查询性能。
数据一致性
读写之间是最终一致性。
适用场景
读写分离明显
读操作远多于写操作,且读写性能要求差异大。
复杂查询
需要进行多维度、复杂的聚合查询,而写操作模型不适合直接查询。