ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CQRS的两种设计方式

CQRS的两种设计方式

明整理: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 并非不能用,但它更适合这些场景:

  • 对审计追踪、可回放、时点恢复有强需求。
返回列表