CQRS

数据库读写分离、本地缓存和分布式缓存本质上都是 CQRS,即将写操作和读操作拆分到不同路径;写数据存储负责可靠更新,读数据存储负责高效查询,二者通过消息队列、主从复制、binlog 监听或定时任务同步数据,从而提升高并发读能力,但代价是架构复杂度增加和读数据存在最终一致性延迟。

一、核心主线

本节是对前面高并发读方案的抽象总结。

无论是:

  • 数据库读/写分离;
  • 本地缓存;
  • 分布式缓存;

它们本质上都在做一件事:

将数据读取路径和数据写入路径分离。

这就是 CQRS。

CQRS 全称是 Command Query Responsibility Segregation,即命令查询职责分离

1
2
Command:会改变数据的操作,如新增、删除、修改
Query:只读取数据、不改变数据的操作

二、CQRS 的基本思想

1. 读写职责分离

CQRS 将系统中的数据操作分为两类:

类型 含义 示例
Command 写操作,会引起数据变化 新增、删除、修改
Query 读操作,只查询数据 查询详情、搜索、列表展示

普通架构中,读和写可能都访问同一份存储:

1
2
3
读请求 ─┐
├─ 同一个数据库
写请求 ─┘

CQRS 中,读和写使用不同路径:

1
2
写请求 → 写数据存储
读请求 → 读数据存储

2. 读写存储可以不同

CQRS 不要求读存储和写存储使用同一种数据库。

更常见的做法是:

  • 写存储选择适合高并发写入、事务和一致性的系统;
  • 读存储选择适合高并发读取、搜索、聚合或缓存的系统。

例如:

1
2
写存储:MySQL
读存储:Redis / Elasticsearch / 宽表 / Slave

三、CQRS 的简要架构

1. 基本链路

CQRS 的典型架构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
客户端
├─ command 写请求
│ ↓
│ 业务服务
│ ↓
│ 写数据存储
│ ↓ 数据变更消息
│ 数据传输通道
│ ↓
│ 读数据存储

└─ query 读请求

业务服务

读数据存储

返回结果

2. 写请求流程

当业务服务收到 command 请求:

  1. 将写请求交给写数据存储;
  2. 写数据存储完成数据变更;
  3. 将数据变更消息发送到数据传输通道;
  4. 读数据存储监听变更消息;
  5. 读数据存储更新自己的数据。

3. 读请求流程

当业务服务收到 query 请求:

  1. 将读请求交给读数据存储;
  2. 读数据存储返回查询结果;
  3. 业务服务将结果返回客户端。

读请求不会访问写存储,从而减轻写存储压力。


四、CQRS 与前面方案的关系

1. 数据库读写分离

数据库读写分离是 CQRS 的一个简单特例。

1
2
3
写数据存储:Master
读数据存储:Slave
数据传输通道:数据库主从复制

写请求进入 Master,读请求进入 Slave。

2. 分布式缓存

分布式缓存也符合 CQRS 思想。

1
2
3
写数据存储:数据库
读数据存储:Redis 缓存
数据传输通道:binlog 监听 / 消息队列 / 缓存更新逻辑

数据库负责保存权威数据,Redis 负责高并发读取。

3. 本地缓存

本地缓存也可以理解为读模型的一种。

1
2
写数据存储:数据库或下游服务
读数据存储:服务进程内本地缓存

本地缓存把部分高频读取数据放到进程内存中,提升读取效率。


五、使用场景一:搜索场景

1. 问题背景

很多应用支持根据关键词搜索用户昵称。

例如搜索“北京”,返回:

  • 北京日报;
  • 北京大学;
  • 这里是北京。

账号信息通常存储在数据库中,但数据库并不擅长高效全文搜索或关键词搜索。

2. CQRS 方案

可以将数据库作为写数据存储,将 Elasticsearch 作为读数据存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
写请求:修改用户昵称

数据库
↓ binlog
消息中间件

消费者服务

更新 Elasticsearch

搜索请求

Elasticsearch

返回搜索结果

3. 架构分工

角色 作用
数据库 管理账号权威数据
Elasticsearch 提供高效搜索能力
消息中间件 传递数据库变更事件
消费者服务 监听变更并同步搜索索引

这种设计让数据库继续承担写入和事务职责,让 Elasticsearch 承担搜索查询职责。


六、使用场景二:多表关联查询

1. 问题背景

运营后台经常需要查询复杂业务数据,这些数据可能分散在多张表中。

如果直接在在线数据库执行多表 JOIN

  • 底层通常依赖嵌套循环,效率较低;
  • 会影响线上数据库性能;
  • 分库分表后,跨库 JOIN 更难执行;
  • 查询延迟不可控。

2. CQRS 方案

提前将多表关联数据聚合计算好,写入一张宽表。

1
2
3
4
5
6
7
数据库变更
↓ binlog / 定时任务
worker 执行数据聚合计算

写入宽表

运营后台查询宽表

查询时直接读取宽表,不再执行复杂 JOIN。

3. 架构分工

角色 作用
数据库 写数据存储,保存原始业务数据
worker 执行数据聚合计算
消息队列 / 定时任务 触发聚合计算
宽表 读数据存储,保存聚合后的查询结果
运营后台 直接读取宽表

4. 数据同步方式

worker 可以通过两种方式触发:

  • 监听数据库 binlog,每次数据变化后实时计算;
  • 定时执行聚合计算,例如每分钟执行一次。

实时性要求高时,适合 binlog + 消息队列;实时性要求低时,可以使用定时任务。


七、CQRS 架构特点

1. 读写存储模型不同

CQRS 中,读数据存储和写数据存储往往有不同的模型和选型。

1
2
写存储:面向事务、写入、一致性
读存储:面向查询、搜索、缓存、聚合

这使得系统可以针对不同访问模式选择最合适的存储系统。

2. 读数据存在延迟

写数据存储发生变化后,读数据存储不是立刻同步完成。

同步过程依赖:

  • 消息队列;
  • binlog 监听;
  • 定时任务;
  • 主从复制;
  • 其他数据传输通道。

因此,CQRS 通常只能保证:

写数据存储和读数据存储之间的最终一致性。

3. 数据传输通道必须可靠

数据传输通道负责把写存储的数据变化同步到读存储。

它必须足够健壮,尽量保证:

  • 数据不丢失;
  • 消息可重试;
  • 消费失败可恢复;
  • 延迟可监控;
  • 积压可处理。

否则,读存储可能长期落后于写存储。


八、CQRS 的优势与代价

1. 优势

  • 写存储和读存储可以独立优化;
  • 读请求不再直接压垮写库;
  • 可以为搜索、聚合、缓存等场景选择专门存储;
  • 能提升高并发读场景的系统吞吐;
  • 支持复杂查询结果预计算;
  • 便于系统按访问模式拆分职责。

2. 代价

  • 架构复杂度上升;
  • 需要维护数据同步链路;
  • 读数据存在延迟;
  • 只能保证最终一致性;
  • 需要处理消息丢失、重复消费、同步失败等问题;
  • 需要监控读写数据之间的同步延迟。

九、核心架构思想

  1. CQRS 的本质是读写职责分离。
  2. Command 表示会改变数据的操作,Query 表示只读取数据的操作。
  3. 数据库读写分离、本地缓存、分布式缓存都可以看作 CQRS 思想的体现。
  4. 写数据存储应选择适合高并发写入和一致性保障的系统。
  5. 读数据存储应选择适合高并发读取、搜索或聚合查询的系统。
  6. 数据传输通道负责将写存储的数据变化同步到读存储。
  7. 搜索场景可以用数据库作为写存储,用 Elasticsearch 作为读存储。
  8. 多表关联查询场景可以用宽表作为读存储,提前保存聚合结果。
  9. CQRS 提升读性能的代价是架构复杂度和读数据延迟。
  10. CQRS 通常追求最终一致性,而不是读写存储之间的强一致。

© 2026 DadaVinCi's Blog

Elegant theme by Shiro · Made by Acris with ❤️