一、为什么需要 CQRS
一个电商平台的商品详情页,用户每次打开都要看到:商品基本信息、价格与库存、用户评价摘要、相关推荐、销量排名。为了拼出这个页面,后端需要 JOIN 五张以上的表,加上缓存失效时的回源查询,数据库压力山大。
但同一时刻,运营人员在后台修改商品价格——只需 UPDATE 一张表的一行。写入操作简单而低频,读取操作复杂而高频。两者对数据模型的需求完全相反:读取需要反范式的宽表来加速查询,写入需要范式化的模型来保证一致性。
传统 CRUD 用同一个模型处理读写,结果就是两头不讨好——查询慢了加索引和冗余字段,写入又变慢;为了写入快拆表,查询又得多 JOIN。CQRS 的思路很直接:既然读和写的需求矛盾,那就别用一个模型了,读写各用各的。
二、现实类比
图书馆。书架区负责存书和借还——书按分类号排列,方便上架和归位,这是「写模型」。卡片目录(现在是检索终端)负责帮你找书——按书名、作者、主题多种维度组织,一条检索结果就包含位置、状态、简介,这是「读模型」。卡片目录是书架内容的一个查询优化投影,两者数据来源相同但结构完全不同。书架上挪了一本书,目录跟着更新,但更新不是即时的——可能隔天才同步,这就是最终一致性。
三、核心思想
CQRS 将系统的数据操作分为两类:命令(Command,改变状态)和查询(Query,读取状态),各自使用独立的数据模型。写模型专注于业务规则和一致性,读模型专注于查询效率,两者通过事件总线异步同步。
写端处理命令,更新写模型,发布事件。读端订阅事件,更新投影(Projection,即读优化的视图)。查询只读投影,永远不碰写模型。两端之间是异步通信,因此存在最终一致性——写入后不是立刻能在读端看到,但最终一定会一致。
3.1 核心数据结构
| 结构 | 职责 | 示例 |
|---|---|---|
| Command | 表达意图,不含业务逻辑 | CreateOrderCommand{userID, items[]} |
| Event | 描述已发生的事实,不可变 | OrderCreatedEvent{orderID, userID, items[], timestamp} |
| Projection | 读优化的物化视图 | OrderSummaryView{orderID, total, status} |
Command 是动词——「创建订单」,Event 是过去式——「订单已创建」。Command 可能被拒绝(库存不足),Event 一定已经发生。区分两者是 CQRS 的基础。
3.2 操作复杂度
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 发送 Command | O(1) | 写入写模型 + 发布事件 |
| 查询 Projection | O(1)~O(log n) | 直接读预计算的投影表 |
| 重建 Projection | O(n) | 重放所有事件 |
| 事件存储 | O(n) 增长 | 只追加,不可变 |
查询快,是因为投影已经提前算好了。代价是事件回放重建投影的开销,以及事件存储的无限增长——需要配合快照机制来缓解。
四、变体与对比
| 模式 | 读写模型 | 一致性 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| CRUD | 同一模型 | 强一致 | 低 | 简单业务,读写比接近 |
| CQRS | 分离模型 | 最终一致 | 中 | 读多写少,读写模型差异大 |
| CQRS + Event Sourcing | 分离 + 事件溯源 | 最终一致 | 高 | 需要完整审计追踪 |
| Event Sourcing(无 CQRS) | 单一事件流 | 强一致 | 中高 | 审计需求为主,查询简单 |
CQRS 和事件溯源(Event Sourcing)经常一起出现,但它们是完全独立的模式。Event Sourcing 的核心是用事件序列代替状态快照来持久化数据,它不一定需要分离读写模型。CQRS 也不一定需要 Event Sourcing——你可以用传统数据库做写模型,只是同步事件到读端。
两者结合时威力最大:事件溯源提供完整的历史记录和重建能力,CQRS 提供灵活的查询投影。但复杂度也最高——你要处理事件版本迁移、投影重建、最终一致性带来的 UI 问题。单独使用 CQRS(不用事件溯源)简单得多,写模型仍然是传统数据库,只是把变更事件推到读端。
五、多语言实现
5.1 Go 实现
package cqrs
import "fmt"
// ---------- Command 与 Event 定义 ----------
// Command 表达写操作意图type Command interface { CommandName() string}
// Event 描述已发生的事实type Event interface { EventName() string}
// CreateOrderCommand 创建订单命令type CreateOrderCommand struct { OrderID string UserID string Items []OrderItem}
func (c CreateOrderCommand) CommandName() string { return "CreateOrder" }
// OrderItem 订单条目type OrderItem struct { ProductID string Quantity int Price float64}
// OrderCreatedEvent 订单已创建事件type OrderCreatedEvent struct { OrderID string UserID string Items []OrderItem Total float64}
func (e OrderCreatedEvent) EventName() string { return "OrderCreated" }
// ---------- 写模型 ----------
// CommandHandler 处理命令,返回事件type CommandHandler func(cmd Command) (Event, error)
// WriteModel 写端模型:保存订单状态,处理命令type WriteModel struct { orders map[string]*Order}
// Order 写端领域对象type Order struct { ID string UserID string Items []OrderItem Total float64 Status string}
func NewWriteModel() *WriteModel { return &WriteModel{orders: make(map[string]*Order)}}
// HandleCommand 处理命令并返回事件func (w *WriteModel) HandleCommand(cmd Command) (Event, error) { switch c := cmd.(type) { case CreateOrderCommand: // 业务规则校验 if len(c.Items) == 0 { return nil, fmt.Errorf("订单不能为空") } total := 0.0 for _, item := range c.Items { total += item.Price * float64(item.Quantity) } // 更新写模型状态 w.orders[c.OrderID] = &Order{ ID: c.OrderID, UserID: c.UserID, Items: c.Items, Total: total, Status: "CREATED", } // 返回事件 return OrderCreatedEvent{ OrderID: c.OrderID, UserID: c.UserID, Items: c.Items, Total: total, }, nil default: return nil, fmt.Errorf("未知命令: %s", cmd.CommandName()) }}
// ---------- 读模型 ----------
// OrderSummary 投影:读优化的订单摘要type OrderSummary struct { OrderID string UserID string Total float64 Status string}
// ReadModel 读端模型:维护物化视图type ReadModel struct { // 按订单 ID 索引的摘要视图 summaries map[string]*OrderSummary // 按用户 ID 索引的订单列表 userOrders map[string][]string}
func NewReadModel() *ReadModel { return &ReadModel{ summaries: make(map[string]*OrderSummary), userOrders: make(map[string][]string), }}
// HandleEvent 订阅事件,更新投影func (r *ReadModel) HandleEvent(evt Event) { switch e := evt.(type) { case OrderCreatedEvent: r.summaries[e.OrderID] = &OrderSummary{ OrderID: e.OrderID, UserID: e.UserID, Total: e.Total, Status: "CREATED", } r.userOrders[e.UserID] = append(r.userOrders[e.UserID], e.OrderID) }}
// QueryByOrderID 查询订单摘要func (r *ReadModel) QueryByOrderID(id string) (*OrderSummary, bool) { s, ok := r.summaries[id] return s, ok}
// QueryByUser 查询用户的所有订单 IDfunc (r *ReadModel) QueryByUser(userID string) []string { return r.userOrders[userID]}
// ---------- 事件总线 ----------
// EventBus 连接写端和读端type EventBus struct { handlers []func(Event)}
func (b *EventBus) Subscribe(handler func(Event)) { b.handlers = append(b.handlers, handler)}
func (b *EventBus) Publish(evt Event) { for _, h := range b.handlers { h(evt) // 同步调用;生产环境通常异步 }}写模型只关心业务规则(订单不能为空、计算总价),读模型只关心查询效率(按订单 ID 索引、按用户 ID 索引)。两者通过 EventBus 解耦,写端不用知道读端怎么存数据。生产环境中 EventBus 通常换成消息队列(Kafka、RabbitMQ),实现真正的异步和持久化。
5.2 TypeScript 实现
// ---------- Command 与 Event 定义 ----------
interface Command { readonly type: string;}
interface Event { readonly type: string; readonly timestamp: number;}
interface OrderItem { productId: string; quantity: number; price: number;}
// 创建订单命令interface CreateOrderCommand extends Command { readonly type: "CreateOrder"; orderId: string; userId: string; items: OrderItem[];}
// 订单已创建事件interface OrderCreatedEvent extends Event { readonly type: "OrderCreated"; orderId: string; userId: string; items: OrderItem[]; total: number;}
// ---------- 写模型 ----------
// 订单领域对象interface Order { id: string; userId: string; items: OrderItem[]; total: number; status: string;}
class OrderWriteModel { private orders = new Map<string, Order>();
// 处理命令,返回事件或抛出异常 handleCommand(cmd: CreateOrderCommand): OrderCreatedEvent { // 业务规则校验 if (cmd.items.length === 0) { throw new Error("订单不能为空"); }
const total = cmd.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 );
// 更新写模型 this.orders.set(cmd.orderId, { id: cmd.orderId, userId: cmd.userId, items: cmd.items, total, status: "CREATED", });
// 返回事件 return { type: "OrderCreated", orderId: cmd.orderId, userId: cmd.userId, items: [...cmd.items], // 防止外部修改 total, timestamp: Date.now(), }; }}
// ---------- 读模型(投影) ----------
// 反范式化的订单摘要视图interface OrderSummaryView { orderId: string; userId: string; total: number; status: string; itemCount: number; // 冗余字段,避免回查明细}
class OrderReadModel { // 投影:按订单 ID 索引 private byOrderId = new Map<string, OrderSummaryView>(); // 投影:按用户 ID 索引 private byUserId = new Map<string, string[]>();
// 订阅事件,更新投影 handleEvent(evt: OrderCreatedEvent): void { // 构建读优化的视图 this.byOrderId.set(evt.orderId, { orderId: evt.orderId, userId: evt.userId, total: evt.total, status: "CREATED", itemCount: evt.items.length, // 预计算,查询时不用遍历 items });
const userOrders = this.byUserId.get(evt.userId) ?? []; userOrders.push(evt.orderId); this.byUserId.set(evt.userId, userOrders); }
// 查询接口——只读投影,不碰写模型 findByOrderId(orderId: string): OrderSummaryView | undefined { return this.byOrderId.get(orderId); }
findByUser(userId: string): string[] { return this.byUserId.get(userId) ?? []; }}
// ---------- 事件总线 ----------
type EventHandler = (evt: Event) => void;
class EventBus { private handlers: EventHandler[] = [];
subscribe(handler: EventHandler): void { this.handlers.push(handler); }
publish(evt: Event): void { for (const handler of this.handlers) { handler(evt); } }}
// ---------- 组装 ----------
const eventBus = new EventBus();const writeModel = new OrderWriteModel();const readModel = new OrderReadModel();
// 读端订阅事件eventBus.subscribe((evt) => { if (evt.type === "OrderCreated") { readModel.handleEvent(evt as OrderCreatedEvent); }});
// 发送命令 → 写端处理 → 发布事件 → 读端更新投影function sendCommand(cmd: CreateOrderCommand): void { const evt = writeModel.handleCommand(cmd); eventBus.publish(evt);}TypeScript 实现用 EventEmitter 风格连接读写两端。读模型的 itemCount 是一个典型的反范式化字段——写端不需要它,但读端查询时直接拿来用,不用回查 items 数组。这就是 CQRS 的核心收益:读端自由地按查询需求组织数据,不用顾虑写端的范式约束。
六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| Axon Framework | github.com/AxonFramework/AxonFramework | Java 生态最成熟的 CQRS + Event Sourcing 框架,提供 Aggregate、CommandBus、EventBus、QueryBus 全套抽象 |
| EventStoreDB | github.com/EventStore/EventStore | 专为事件溯源设计的数据库,内置事件存储和订阅机制,天然适配 CQRS |
| MediatR | github.com/jbogard/MediatR | .NET 生态的中介者模式库,IRequestHandler<TCommand> 和 IRequestHandler<TQuery, TResult> 天然分离读写 |
| Lagom | github.com/lagom/lagom | Lightbend 出品的微服务框架,CQRS + Event Sourcing 为一等公民 |
Axon Framework 是 CQRS 领域的标杆实现。它的 CommandGateway 发送命令到 Aggregate(写模型),EventProcessor 将事件投影到读模型的 QueryHandler。整个读写分离的生命周期——命令路由、事件存储、投影重建、 Sagas 编排——都有开箱即用的支持。
七、小结
何时使用:
- 读多写少且查询复杂——电商商品页、社交 Feed、报表系统,读端可以自由反范式化
- 读写模型差异大——写端需要严格校验和业务规则,读端需要多种维度的快速查询
- 与 Event Sourcing 配合——事件溯源提供完整审计追踪,CQRS 提供灵活查询能力
- 团队可以分治——写端和读端由不同团队独立开发、独立部署、独立扩缩容
何时不用:
- 简单 CRUD 应用——一个模型就能搞定读写,CQRS 增加的复杂度远大于收益
- 强一致性需求——金融交易等场景要求写入后立刻读到最新状态,最终一致性不可接受
- 团队规模小——维护两套模型、事件同步、投影重建需要额外的人力和运维成本
- 领域模型简单——没有复杂的业务规则,读写逻辑用同一套模型毫无压力
CQRS 不是银弹,它是一种用复杂度换性能和灵活性的权衡。引入 CQRS 之前,先问自己:单模型真的撑不住吗?如果答案是「还好」,那就不需要 CQRS。
八、参考资料
- CQRS - Martin Fowler - Martin Fowler 对 CQRS 的定义与适用性分析
- CQRS and Event Sourcing - Greg Young - Greg Young 2010 年的经典演讲,CQRS 概念的起源
- Axon Framework Reference - Java CQRS + Event Sourcing 框架的完整文档
- EventStoreDB Documentation - 事件存储数据库的设计理念和使用指南
- CQRS Pattern - Microsoft - Azure 架构中心对 CQRS 模式的系统性解读
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






