CQRS的两种设计方式

我们在查阅 Domain-Driven Design(DDD)相关资料时,经常会看到 CQRS(Command Query Responsibility Segregation)。继续深挖后又会牵出 Event Sourcing、Outbox、最终一致性等概念,越看越容易混乱。本文做一次简明整理:CQRS 到底有哪几种常见设计方式,以及在实际项目中该如何选择。

版本 1:Event Sourcing + CQRS(最常见的讨论版本)

上面这张图是最常见的 Event Sourcing + CQRS 架构,流程大致如下:

  1. Client 发送 Command 请求到 Command Service。
  2. Command Service 执行 Aggregate(聚合根)业务逻辑,产生 Domain Event,并写入 Event Store。
  3. 同时将 Domain Event 发布到 Event Bus。
  4. Query Service 订阅 Event Bus 并接收 Domain Event。
  5. Query Service 将变更同步到查询侧数据库(Read Model DB)。

下面先看 Event Sourcing(ES)的优缺点。

最开始只有 Event Sourcing

Event Sourcing 严格来说是在 DDD 圈子里被大规模推广的,其核心概念是:

数据库只存 Domain Event,而不是传统的 Domain State(领域对象当前状态)。

因为在 DDD 中,Aggregate 的变化会产生 Domain Event,所以理论上只保留事件流也可以重建最终状态。

这样做的好处通常有:

  1. 有所有事件的存档,意味着可以恢复到任一时间点下的状态。
  2. 因为有整个状态的变更历史记录,有利于排查问题。
  3. 一定程度上减少状态与事件"双写不一致"问题。若采用状态存储,需要确保状态持久化与事件发布在同一事务边界内;而 Event Sourcing 天然以事件为事实来源。
  4. 可以降低并发写冲突复杂度。事件通常是追加写,不是原地更新(但仍需要处理 duplicate event 和幂等)。

PS:第 3 点在非 Event Sourcing 架构里也可以通过 transactional outbox(发件箱模式)来解决。

为什么这里会出现 CQRS?

因为每次查询都从事件流重放来恢复状态,读取成本高,所以才需要 CQRS。

在大多数业务系统里,Query 次数远大于 Command。纯 ES 的"写优化"并不能直接满足查询性能诉求,因此通常要引入读模型(Read Model),也就是 CQRS 中的 Query 模式。

这个版本有什么问题?

  • Write Service 不直接保存对象最终状态,某些"先读再写"的业务判断会变复杂,且会遇到读写延迟。
  • 存储与运维成本较高(事件量膨胀、回放成本、快照策略、归档策略等)。
  • 事件模型演进困难:当事件结构变化时,历史事件兼容、重放逻辑、Query 侧投影都要一起考虑。
  • 数据同步是最终一致性,不是强一致性。
  • 事务边界与失败补偿设计复杂(发布失败、重复消费、顺序保证、幂等处理等)。

版本 2:读写分离(更常见的工程化 CQRS)

我认为这才是 CQRS 在工程实践中的主流形态:将模型拆分为两套职责。

  • Write:用 DDD 处理业务规则、状态变更与持久化。
  • Query:围绕 UI 与报表需求,做高性能、可定制查询。

Write 与 Query 分开的原因在于:

Aggregate Model 并不天然适合查询场景:查询成本高,且字段结构不一定匹配前端展示需求。

这种设计方式的常见好处:

  • 职责边界清晰:哪些是 Command、哪些是 Query 一目了然,能显著减少在查询流程里"顺手改数据"的问题。
  • 模型解耦:不会为了查询便利去污染 Aggregate 模型,符合单一职责原则。
  • 支持物理隔离与异构存储:例如写入 MySQL、同步到 PostgreSQL 或 Elasticsearch 做查询。

我在项目里通常采用这种方式。很多开发者也有类似实践,例如 Amichai Mantinband 在视频 Understand Clean Architecture in 7 Minutes 中提到的设计,本质上也是在 Application 层做命令与查询分离。

小结

实际项目中,大多数团队会优先落地"读写分离版 CQRS",而不是"Event Sourcing + CQRS 全家桶"。

Event Sourcing + CQRS 并非不能用,但它更适合这些场景:

  • 对审计追踪、可回放、时点恢复有强需求。
  • 团队具备事件建模、投影构建、幂等消费、事件版本演进的工程能力。
  • 业务复杂度和收益足够覆盖其架构成本。

如果你的目标是"让系统边界清晰、查询性能更好、代码职责更干净",从读写分离开始通常是更稳妥的选择。

相关推荐
凤山老林3 天前
DDD(领域驱动设计)在复杂业务系统中的落地指南
java·开发语言·数据库·ddd·领域驱动
co_der4 天前
救救孩子吧:被 AI 蠢哭后,我手把手教它 DDD,最后逼它自己写了这篇总结
ai编程·领域驱动设计
刀法如飞10 天前
DDD 与 Ontology 对比分析:哪一种更适合AI时代复杂系统构建?
java·架构·领域驱动设计
魏杨杨10 天前
被流量逼出来的架构:从一台服务器到云原生的 17 次蜕变 —— 集群、缓存、MQ、微服务、Docker、K8S 的前世今生
微服务·k8s·负载均衡·ddd·分部署
幸福的猪在江湖12 天前
5 万 Star!OpenSpec 规范驱动开发完全指南:让 AI 按你的规矩写代码
aigc·ai编程·领域驱动设计
Esofar14 天前
Dddify:给 ASP.NET Core 项目一套轻量、清晰、可落地的 DDD 基础设施
c#·ddd·asp.net core·cqrs·dddify·clean architecture
weixin_4223293120 天前
一文学会DDD领域驱动设计
ddd·领域设计
rolt1 个月前
[幻灯片]分析设计高阶-02结构(2)-202604更新
ddd·架构师·uml·ooad
王大大的刀1 个月前
《DDD 六边形架构入门:8000 字带你彻底搞懂聚合根、领域服务、防腐层》
领域驱动设计