CQRS + Event Sourcing(读写分离 + 事件溯源)
解决什么问题:读写用同一个模型 = 必然互相妥协—— 查询要"扁平、聚合、宽表",写入要"严格校验、不变量保护、领域规则"——一个模型装不下两个目标。 CQRS = 把读写拆成两个独立模型;ES = 把写模型从"当前状态"换成"事件流"。 两者合用 = 写模型专注业务规则,读模型为查询优化,事件流 = 业务事实的不可变历史。
它是什么
CQRS(Command Query Responsibility Segregation): 读写用不同模型——
- 写模型(Command) = 聚合根 + 领域逻辑 + 不变量
- 读模型(Query) = 视图(宽表 / 索引 / 物化视图)按查询场景优化
ES(Event Sourcing): 不存"当前状态",存"导致当前状态的所有事件"——
Order 当前状态 = sum(订单创建 + 支付完成 + 发货 + 签收)
= Aggregate[OrderCreated, OrderPaid, OrderShipped, OrderReceived]状态由事件流物化(materialize)出来。
为什么 CQRS + ES 通常合用:
- ES 的"事件流"天然是只追加的(写)
- ES 的"物化视图"天然是查询优化的(读)
- 两者边界正好 = CQRS 的边界
EDM Ch 10(Bellemare)原话: "By separating read and write models, you can evolve each independently... Event Sourcing makes the write side a stream of immutable facts, and the read side becomes a materialized view of those facts."
IDDD Ch 8(Vernon)原话: "Domain Events represent the facts of what happened in the past... Aggregates are responsible for applying commands and emitting events."
V2 视角(V2.7 B3 段 5 分解 + B5 段 3 反馈 + B7 段 5 自然演化): CQRS = 写权集中(Single Writer Principle)——每个聚合只有一个微服务写。 ES = 事件流 = 跨 BC 演化的 SSOT(EDM Ch 17)。 两者合用 = 写权集中 + 演化无锁——这是软件工程对"分工 + 演化"的极致回应。
关键属性
| 属性 | 值/含义 |
|---|---|
| 读写分离 | Command 模型 ≠ Query 模型,各自独立演化 |
| 事件不可变 | 已发布事件不可修改,只能追加新事件 |
| 状态可重放 | 同一事件流可重放得到同一状态(确定性) |
| 单写者 | 一个聚合只有一个写者(Single Writer Principle) |
| 多读模型 | 同一事件流可投影成多个读模型(按查询场景) |
| 最终一致 | 读写模型之间有传播延迟(毫秒到秒) |
| 审计免费 | 事件流 = 完整审计日志 |
| 时间旅行 | 可查询"任意时刻的状态" |
| 反模式 | 读写用同一模型 / 用 ES 但不分离读写 / 事件不版本化 |
| 判错标志 | 业务"明明有变化但查不到" / 改了一个聚合影响多个读模型 |
怎么用
场景 1:CQRS 基础——读写模型分离
# ❌ 传统 CRUD(一套模型)
class Order(Model):
def place(self): ... # 写
def find_by_id(id): ... # 读
def search_by_status(s): ...# 读
# 写入字段 + 查询字段互相妥协
# ✅ CQRS(两套模型)
class OrderCommandService:
def place_order(cmd: PlaceOrderCommand): ...
def cancel_order(cmd: CancelOrderCommand): ...
class OrderQueryService:
def find_by_id(id) -> OrderView: ...
def search_by_status(status) -> list[OrderView]: ...
def get_customer_order_history(customer_id) -> list[OrderView]: ...
# 写模型 = 领域逻辑 + 不变量
# 读模型 = 宽表 + 索引 + 按查询优化场景 2:Event Sourcing——用事件流代替状态
# 事件定义(不可变事实)
@dataclass(frozen=True)
class OrderCreated:
order_id: str
customer_id: str
items: list[OrderItem]
created_at: datetime
@dataclass(frozen=True)
class OrderPaid:
order_id: str
paid_amount: Money
paid_at: datetime
@dataclass(frozen=True)
class OrderShipped:
order_id: str
tracking_number: str
shipped_at: datetime
# 聚合根 = 应用事件 + 发新事件
class Order:
def __init__(self, events: list):
self.events = events
# 从事件流重建状态
self.state = self._replay(events)
def _replay(self, events):
state = {}
for event in events:
state = self._apply(state, event)
return state
def place(self, cmd):
# 业务规则校验
if self.state.get('status') == 'paid':
raise OrderAlreadyPaid(cmd.order_id)
# 发新事件
return [OrderCreated(...)]
def pay(self, cmd):
if self.state.get('status') != 'placed':
raise OrderNotPlaced(cmd.order_id)
return [OrderPaid(...)]
def ship(self, cmd):
if self.state.get('status') != 'paid':
raise OrderNotPaid(cmd.order_id)
return [OrderShipped(...)]
# 仓储:只存事件,不存状态
class OrderRepository:
def save(self, order):
for event in order.events:
self.event_store.append(event) # 追加到事件流
def load(self, order_id):
events = self.event_store.get(order_id)
return Order(events) # 从事件流重建场景 3:CQRS + ES——读模型从事件流投影
# 读模型 1:订单详情视图(聚合多个事件)
class OrderDetailView:
order_id: str
customer_id: str
items: list
status: str
total_amount: Money
paid_at: datetime
shipped_at: datetime
# 投影器:消费事件流,更新读模型
class OrderDetailProjector:
def on_OrderCreated(self, event):
OrderDetailView.upsert(
order_id=event.order_id,
customer_id=event.customer_id,
items=event.items,
status='created',
)
def on_OrderPaid(self, event):
OrderDetailView.update(
order_id=event.order_id,
status='paid',
paid_at=event.paid_at,
)
def on_OrderShipped(self, event):
OrderDetailView.update(
order_id=event.order_id,
status='shipped',
shipped_at=event.shipped_at,
)
# 读模型 2:客户订单历史(按查询场景优化)
class CustomerOrderHistoryView:
customer_id: str
orders: list # 反范式 + 索引
total_spent: Money
# 同一个 OrderCreated 事件,可投影成多个读模型场景 4:单写者原则(Single Writer Principle)
EDM Ch 7:每个聚合只能由一个微服务写入。
# Order 聚合的写者
OrderService:
写权限: 全权(包括创建、修改、状态流转)
读权限: 可以读自己的写模型
发事件: OrderCreated, OrderUpdated, OrderCancelled
# 其他服务只能读或订阅事件
InventoryService:
写权限: 仅自己的 Inventory 聚合
读权限: 订阅 OrderCreated → 扣库存
订阅: OrderCreated, OrderCancelled
PaymentService:
写权限: 仅自己的 Payment 聚合
订阅: OrderCreated → 创建支付单单写者原则的好处:
- 无并发冲突(不需要分布式锁)
- 状态可重放(事件流 = 真相)
- 演化无锁(每个聚合独立)
场景 5:聚合从事件流重建(State Materialization)
# 仓储从事件流物化聚合
class OrderRepository:
def load(self, order_id) -> Order:
# 从事件流读所有该订单的事件
events = self.event_store.get_stream(order_id)
# 物化聚合
return Order(events)
def save(self, order: Order, new_events: list):
# 追加新事件到事件流
for event in new_events:
self.event_store.append(order.id, event)
# 更新读模型(异步投影)
self.projector.project(new_events)V2 B1 认识对象视角——事件流 = SSOT,聚合 = 事件流的物化视图,读模型 = 事件流的多个投影。 三层都是"事件流的投影"——区别只是"投影到哪里、用什么 schema"。
场景 6:Snapshot(避免每次重放全部事件)
class OrderRepository:
SNAPSHOT_EVERY = 100 # 每 100 个事件存一次 snapshot
def load(self, order_id):
# 读最近的 snapshot
snapshot = self.snapshot_store.get(order_id)
# 读 snapshot 之后的事件
events_after = self.event_store.get_stream_since(
order_id, snapshot.event_number
)
# 从 snapshot + 新事件重建
return Order.from_snapshot_and_events(snapshot, events_after)Snapshot 解决 = ES 的性能问题——事件无限增长,每次都从 0 重放太慢。
常见误区
- "CQRS = 微服务"——错。CQRS 可以单体部署(读写分两个进程),微服务是部署方式,CQRS 是架构模式
- "ES 必须配合 CQRS"——错。ES 可以单独用(写完同步读),但 CQRS+ES 是最常见的搭配
- "事件可以修改"——错。事件 = 不可变事实,要改只能追加新事件 + 创建补偿事件
- "读写强一致"——错。CQRS 的本质是最终一致,读写模型之间有传播延迟
- "所有业务都用 CQRS+ES"——错。简单 CRUD 用 CQRS+ES 是过度设计。用得上的标志 = 业务复杂 + 查询多样 + 需要审计
- "快照可以替代事件"——错。快照只是性能优化,事件流才是 SSOT。删事件 = 删真相
- "事件 schema 可以随便改"——错。事件 schema 演化受 Event Contract 规则约束
- "读模型可以随便改"——半对。读模型可以重建(重放事件流),但已建好的读模型不能直接改——只能全量重建
关键洞察
CQRS = 读写分离让写权集中 + 读灵活。ES = 用事件流代替当前状态,让"事实"成为 SSOT。两者合用 = 写权集中(Single Writer)+ 演化无锁(事件流投影)= 团队可拥有 + 业务可演化。
V2 B3 分解的极致应用—— 不是切微服务,是切"写权"。每个聚合的写者唯一,事件流 = 跨聚合的协调机制。 切得对 = 1 个团队拥有 1 个聚合的写权 + N 个读模型的订阅权。
V2 B5 反馈视角—— 读模型是"事件流的反馈投影",不是"当前状态的镜像"。 反馈会有延迟(最终一致),所以读模型要容忍延迟。
判断 CQRS+ES 用得对不对的标志:
- 事件流是 SSOT(删事件 = 删业务)
- 每个聚合只有 1 个写者
- 读模型可重建(重放事件流)
- 业务变化时只追加事件,不改事件
相关链接
- Saga——CQRS+ES 在跨 BC 协调中的应用
- Event Driven Workflow——事件驱动工作流
- Event Contract——事件 schema 演化规则
- [V3 涌现作为] · 分解——CQRS 是分解的极致
- [V3 涌现之时] · 演化——ES 让演化"自然"
- [V3 涌现作为] · 反馈——读模型是事件流的反馈投影