Skip to content

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 基础——读写模型分离 ​

python
# ❌ 传统 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——用事件流代替状态 ​

python
# 事件定义(不可变事实)
@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——读模型从事件流投影 ​

python
# 读模型 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:每个聚合只能由一个微服务写入。

yaml
# Order 聚合的写者
OrderService:
  写权限: 全权(包括创建、修改、状态流转)
  读权限: 可以读自己的写模型
  发事件: OrderCreated, OrderUpdated, OrderCancelled

# 其他服务只能读或订阅事件
InventoryService:
  写权限: 仅自己的 Inventory 聚合
  读权限: 订阅 OrderCreated → 扣库存
  订阅: OrderCreated, OrderCancelled

PaymentService:
  写权限: 仅自己的 Payment 聚合
  订阅: OrderCreated → 创建支付单

单写者原则的好处:

  • 无并发冲突(不需要分布式锁)
  • 状态可重放(事件流 = 真相)
  • 演化无锁(每个聚合独立)

场景 5:聚合从事件流重建(State Materialization) ​

python
# 仓储从事件流物化聚合
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(避免每次重放全部事件) ​

python
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 重放太慢。

常见误区 ​

  1. "CQRS = 微服务"——错。CQRS 可以单体部署(读写分两个进程),微服务是部署方式,CQRS 是架构模式
  2. "ES 必须配合 CQRS"——错。ES 可以单独用(写完同步读),但 CQRS+ES 是最常见的搭配
  3. "事件可以修改"——错。事件 = 不可变事实,要改只能追加新事件 + 创建补偿事件
  4. "读写强一致"——错。CQRS 的本质是最终一致,读写模型之间有传播延迟
  5. "所有业务都用 CQRS+ES"——错。简单 CRUD 用 CQRS+ES 是过度设计。用得上的标志 = 业务复杂 + 查询多样 + 需要审计
  6. "快照可以替代事件"——错。快照只是性能优化,事件流才是 SSOT。删事件 = 删真相
  7. "事件 schema 可以随便改"——错。事件 schema 演化受 Event Contract 规则约束
  8. "读模型可以随便改"——半对。读模型可以重建(重放事件流),但已建好的读模型不能直接改——只能全量重建

关键洞察 ​

CQRS = 读写分离让写权集中 + 读灵活。ES = 用事件流代替当前状态,让"事实"成为 SSOT。两者合用 = 写权集中(Single Writer)+ 演化无锁(事件流投影)= 团队可拥有 + 业务可演化。

V2 B3 分解的极致应用—— 不是切微服务,是切"写权"。每个聚合的写者唯一,事件流 = 跨聚合的协调机制。 切得对 = 1 个团队拥有 1 个聚合的写权 + N 个读模型的订阅权。

V2 B5 反馈视角—— 读模型是"事件流的反馈投影",不是"当前状态的镜像"。 反馈会有延迟(最终一致),所以读模型要容忍延迟。

判断 CQRS+ES 用得对不对的标志:

  • 事件流是 SSOT(删事件 = 删业务)
  • 每个聚合只有 1 个写者
  • 读模型可重建(重放事件流)
  • 业务变化时只追加事件,不改事件

相关链接 ​

从名家方法论与工程化思路中蒸馏出自己的工程体系。