用途
本技能用于在读写模型需要分离时指导 CQRS(Command Query Responsibility Segregation,命令查询职责分离)的落地。其核心是将用于命令(状态变更操作)的模型与用于查询(只读视图)的模型分离,使二者能够独立演进、扩展和持久化。
何时使用
- 读负载与写负载呈显著不对称——每次写入对应大量查询,或反之。
- 不同读用例需要同一数据的不同形状(仪表盘、搜索、详情页)——单一规范读模型只能是折中。
- 天然的写模型(例如带有丰富不变量的聚合)在查询视图时很别扭。
- 写侧受益于事件溯源,而读侧需要快速 SQL 查询——CQRS 是两者之间的桥梁。
何时不要使用
- 读写大致均衡且使用相同形状——CQRS 纯属额外开销。
- 团队不熟悉最终一致性——CQRS 按设计引入了它。
- 系统很小,普通仓储模式即可——属于过度设计。
- 用 "CQRS" 为同一数据建两张数据库表却没有真正驱动因素——那只是数据重复。
核心内容
两个模型,一个底层真相
- 写模型:接收命令(PlaceOrder、CancelShipment),执行校验、强制不变量、持久化。通常是一个聚合(ddd-aggregate)。为一致性而非读取做优化。
- 读模型:接收查询,返回为特定消费者塑形的 DTO,可以是反规范化、预先连接、缓存、为搜索建立索引的。不强制不变量——它只是一个投影。
二者如何同步
写入产生事件(通常是领域事件,参见 ddd-domain-event)。读模型是投影——处理器消费事件并更新物化视图。读侧总是略微落后于写侧,这就是最终一致性,需要有意识地接受它。
存储形态
- 同一数据库、两个模型:最简单。写模型与读模型是同一数据库中的不同表(甚至不同 schema)。读模型由触发器、定时任务或进程内投影填充。
- 不同数据库:写入事务型存储(如 Postgres 存聚合),投影到查询存储(Elasticsearch 做搜索、Redis 做热查找、单独的 Postgres schema 做仪表盘)。带来真实的运维复杂度,需以真实读取需求来证明其合理性。
与事件溯源的关系
CQRS 不要求事件溯源。二者常被搭配,但彼此独立。可以在普通 CRUD 写模型之上做 CQRS,只要它在提交时发出事件——事件作为真相来源的场景参见 arch-event-sourcing。
最终一致性的沟通
读模型会滞后。如果领域专家说"用户期望在仪表盘上立刻看到自己的更改",你有两个选择:收紧投影延迟,或接受使用"你的更改正在处理中"的 UI 提示。不要假装这个时间差不存在。
决策启发式
- 当存在可度量的读写不对称,或写入与读取的形态有实质性差异时使用;不要因为"CQRS 听起来先进"而使用。
- 优先采用单数据库 CQRS(一个 Postgres 中的两个模型)。只有当读存储的特性(搜索、地理空间、内存)确实不同于写存储时,才使用不同数据库。
- 仅当事件存在下游消费者时才与事件驱动搭配;否则更简单的投影即可。
- 读模型是投影,不是真相来源。如果发现自己先从读模型读取再写回,就已经破坏了 CQRS。
反模式
- 只有一个模型的 "CQRS":有两个名为 commands/ 与 queries/ 的文件夹,却读写同一张表且无分离。只是做表面功夫。
- 读模型变成写穿透:代码直接写入读存储,绕过写模型,二者会悄然分叉。
- 对 UI 隐藏最终一致性:用户做出更改,UI 显示陈旧数据,用户以为更改失败。需要传达延迟。
- 沉重的读模型迁移:每次 schema 变更都需要完整重投影,缓慢且高风险。应给读模型做版本管理并并排迁移。
- 为读写对等(无不对称)而采用:纯属额外开销,没有收益。
相关技能
- arch-event-driven——事件通常将写入传播到读模型投影。
- arch-event-sourcing——当写侧以事件作为真相来源存储时,CQRS 成为自然的读侧。
- ddd-aggregate——CQRS 写模型的典型形态。
- ddd-repository——读侧的读取通常不经过领域仓储,而是独立的查询对象。
参考资料
- Khononov, Learning DDD, 第 8 章(CQRS)。
- Greg Young, "CQRS Documents by Greg Young"——CQRS 的原始阐述。
- Vaughn Vernon, Implementing Domain-Driven Design——第 4 章包含对 CQRS 的论述。