架构(04):CQRS 与 Event Sourcing——读写分离、事件溯源、重放
更新时间:2026-09-02。本文是
architecture/架构领域第 04 篇,接 DDD 领域驱动设计。CQRS 把命令和查询分开,适合复杂业务的读写差异大。Event Sourcing 不存状态,只存事件,状态通过事件重放重构。两者经常一起用,但也可以分开用。
本文要回答的问题
- CQRS 是什么?和传统 CRUD 读写分离有什么区别?
- Event Sourcing 是什么?为什么不存当前状态,只存事件?
- 事件重放是什么?怎么用事件重构状态?
- CQRS+ES 适合什么场景,不适合什么场景?
一、传统 CRUD 模型
传统 CRUD:命令和查询用同一个模型
┌────────────────┐
│ 同一个数据模型 │
│ 写 → 更新状态 │
│ 读 → 查询状态 │
└────────────────┘
优点:简单,适合大部分场景
缺点:
写模型需要事务一致性
读模型需要查询性能
两者要求冲突,无法同时满足
复杂查询索引不好建,性能差二、CQRS——命令查询责任分离
CQRS = Command Query Responsibility Segregation
核心:命令(写)和查询(读)分开
┌────────────┐ ┌────────────┐
│ Command │ → │ Write DB │
│ (写操作) │ │ (写模型) │
└────────────┘ └────────────┘
↓ ↓
┌────────────┐ ┌────────────┐
│ Query │ ← │ Read DB │
│ (读操作) │ │ (读模型) │
└────────────┘ └────────────┘核心思想:
- 命令:改变状态,要求一致性,用事务
- 查询:只读,不改变状态,要求性能
- 写模型和读模型分开设计,互不影响
写模型: 适合事务,聚合根,一致性,小 读模型: 适合查询,宽表,预计算,性能好
三、和传统读写分离对比
| 对比 | CQRS | 传统读写分离(数据库主从) |
|---|---|---|
| 分离层级 | 应用层模型分离 | 数据库层面主从分离 |
| 读模型 | 可以定制,预计算 | 和主库一样 |
| 一致性 | 最终一致性 | 最终一致性 |
| 复杂度 | 高 | 低 |
什么时候用 CQRS:
- 读写负载差异大(读多写多,读并发远大于写)
- 读模型需要多种不同查询视图
- 写模型有复杂业务逻辑,读模型要简单查询
- 复杂领域,需要高性能
什么时候不用:
- 简单 CRUD → 传统就够,CQRS 增加复杂度
- 读写负载差不多 → 不需要
四、Event Sourcing——事件溯源
Event Sourcing:不存储当前状态,只存储所有改变状态的事件
传统存储:存当前状态,修改覆盖
id | name | status
1 | 订单A | 已支付
Event Sourcing:存储所有事件,状态由事件重放得到
1 | 事件1 | OrderCreated
1 | 事件2 | PaymentReceived
1 | 事件3 | OrderShipped
当前状态 = 按顺序重放所有事件优点:
- 完整审计日志:所有变化都有记录,可追溯
- 时间旅行:可以看任意时间点的状态
- 重构状态:改了状态模型,可以重放所有事件得到新状态
- 和 DDD 契合:领域事件本来就是 DDD 的概念
缺点:
- 事件数量大,存储大
- 事件变了,需要迁移
- 快照需要维护,不然重放太慢
- 复杂度高,调试困难
五、CQRS + Event Sourcing 结合
CQRS + ES 是常见组合:
写端:
1. 用户发命令
2. 聚合根执行,产生领域事件
3. 把事件 append 到事件存储
4. 发布事件到消息队列
读端:
1. 订阅事件
2. 更新读模型(投影)
3. 查询直接从读模型返回
优势:
写端简单,只有 append,快
读模型预计算,查询快
完整审计日志投影(Projection):
- 投影就是把事件流转换成读模型
- 每次事件发生,投影更新读模型
- 如果读模型坏了,可以重放所有事件重新投影
快照(Snapshot):
- 事件太多,每次重放太慢,做快照
- 每 N 个事件存一次当前状态快照
- 重构状态只需要从快照开始重放后面的事件
六、优缺点和适用场景
优点
| 优点 | 说明 |
|---|---|
| 读写各自优化 | 写做事务,读做查询,互不影响 |
| 完整审计 | 所有事件都在,可追溯 |
| 时间旅行 | 可以看任意时间点的状态 |
| 适合事件驱动 | 和消息驱动架构契合 |
缺点
| 缺点 | 说明 |
|---|---|
| 复杂度高 | 需要处理事件顺序、重复、幂等 |
| 存储开销大 | 每个事件都存,事件数大 |
| 一致性 | 读模型最终一致,有延迟 |
| 调试难 | 问题出在事件流,难定位 |
适用场景
✅ 推荐用:
- 复杂业务领域,读写差异大
- 需要审计追溯(金融、订单、交易)
- 事件驱动架构
- 需要时间旅行调试
- DDD + 微服务
❌ 不推荐用:
- 简单 CRUD
- 业务简单,读写差异不大
- 团队规模小,没人维护复杂度
- 性能要求极高,不能接受最终一致
七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 事件版本变化 | 旧事件格式和新代码不兼容 | 事件版本化,迁移事件 |
| 投影失败 | 读模型和事件不一致 | 幂等投影,可重放 |
| 事件顺序乱 | 重放顺序不对,状态错 | 严格按事件 ID 顺序 |
| 事件太多重放慢 | 每次重放全量慢 | 做快照,从快照开始 |
| 过度设计 | 简单 CRUD 也用 CQRS+ES | 复杂度暴涨 |
相关与延伸
下一篇:消息驱动架构——事件驱动、编排 vs Choreography、死信、幂等消费;DDD 领域驱动设计,见 DDD 领域驱动设计——通用语言、聚合。
一句话总结
CQRS 与 Event Sourcing:CQRS 把命令(写)和查询(读)责任分离,写模型适合事务一致性,读模型适合查询性能,和传统数据库主从读写分离不同,CQRS 是应用层模型分离;Event Sourcing 不存当前状态,只存所有改变状态的事件,当前状态通过事件重放得到,优点是完整审计日志、可追溯、时间旅行;CQRS+ES 常见组合,写端 append 事件,读端投影更新读模型,读模型最终一致;适合复杂业务需要审计,不适合简单 CRUD;常见坑:事件版本变化需要版本化,重放慢需要快照,读模型需要幂等可重放。