title: "16 现代架构扩展(选读:CQRS/ES/Saga)"
学习目标
- 理解 CQRS(Command Query Responsibility Segregation)的核心思想:读写分离
- 理解 Event Sourcing(事件溯源)的核心思想:用事件流表示状态
- 理解 Saga/Process Manager 的核心思想:跨聚合/跨 BC 的流程编排
- 重点:掌握"何时用/何时不用",避免过度设计
核心概念
CQRS(命令查询职责分离)
什么是 CQRS
CQRS 把"写模型(Command Model)"和"读模型(Query Model)"分离:
- 写模型:领域模型(聚合、实体、值对象),用于处理命令
- 读模型:优化的查询视图(DTO、投影),用于处理查询
何时用 CQRS
适合场景:
- 读写比例差异大(例如读多写少)
- 读写性能需求不同(例如写需要强一致性,读可以最终一致性)
- 查询复杂度高(例如需要跨多个聚合的复杂报表)
不适合场景:
- 简单的 CRUD 应用
- 读写比例接近
- 团队规模小,维护成本敏感
伪代码示例
text
// 写模型(Command Side)
class OrderApplicationService {
createOrder(cmd):
order = new Order(...)
repository.save(order)
publish(OrderCreated)
}
// 读模型(Query Side)
class OrderQueryService {
getOrderList(filters):
return readModelRepository.findByFilters(filters) // 优化的查询视图
}
Mermaid 图:CQRS 读写分离(概念)
project
User/UI
Commands
Queries
Command Side
Domain Model + Repository
Events
Query Side
Read Model
Event Sourcing(事件溯源)
什么是 Event Sourcing
Event Sourcing 用事件流来表示聚合的状态:
- 不直接保存聚合的当前状态
- 保存所有导致状态变化的事件
- 通过重放事件来重建聚合状态
何时用 Event Sourcing
适合场景:
- 需要完整的审计日志(所有状态变化都有记录)
- 需要时间旅行(可以查询任意时间点的状态)
- 需要事件重放(例如调试、数据分析)
不适合场景:
- 简单的 CRUD 应用
- 事件流很长,重建状态成本高
- 团队对 Event Sourcing 不熟悉
伪代码示例
说明:以下为 概念性伪代码 。本书不提供 Event Sourcing 的参考实现(例如
EventStore、以及Order.apply(...)的重放逻辑)。
text
// 保存事件(而不是保存状态)
class EventSourcedOrderRepository {
save(order):
events = order.getDomainEvents()
for event in events:
eventStore.append(order.orderNumber, event)
}
// 重建聚合(通过重放事件)
class EventSourcedOrderRepository {
findById(orderNumber):
events = eventStore.getEvents(orderNumber)
order = new Order()
for event in events:
order.apply(event) // 重放事件
return order
}
Mermaid 图:Event Sourcing(概念)
replay
project
Command
Aggregate
Domain Events
Event Store
Aggregate Rehydration
Read Model/Projection
Saga / Process Manager(流程编排)
什么是 Saga
Saga 用于管理跨聚合/跨 BC 的长时间运行流程:
- 一个 Saga 协调多个聚合/BC 完成一个业务目标
- 每个步骤都是独立的命令,可以独立回滚
- 通过补偿事务(Compensating Transaction)处理失败
何时用 Saga
适合场景:
- 跨多个聚合/BC 的复杂流程(例如"下单 → 支付 → 发货 → 通知")
- 需要最终一致性(不能在一个事务里完成)
- 需要处理部分失败(例如支付成功但发货失败,需要退款)
不适合场景:
- 可以在一个事务里完成的简单流程
- 不需要跨聚合/BC 的流程
- 团队对分布式事务不熟悉
伪代码示例
text
// Saga 编排器
class OrderFulfillmentSaga {
start(orderId):
// 步骤1:创建订单
orderCreated = orderService.createOrder(orderId)
if orderCreated.failed:
return saga.fail()
// 步骤2:支付订单
paymentResult = paymentService.pay(orderId)
if paymentResult.failed:
orderService.cancel(orderId) // 补偿
return saga.fail()
// 步骤3:发货
shipmentResult = logisticsService.ship(orderId)
if shipmentResult.failed:
paymentService.refund(orderId) // 补偿
orderService.cancel(orderId) // 补偿
return saga.fail()
return saga.complete()
}
Mermaid 图:Saga/Process Manager(概念)
Logistics Context Payment Context Order Context Saga / Process Manager Logistics Context Payment Context Order Context Saga / Process Manager alt [ship failed] alt [payment failed] [payment succeeded] CreateOrder 1 OrderCreated 2 Pay(orderId) 3 PaymentSucceeded/Failed 4 CancelOrder (compensate) 5 Ship(orderId) 6 Shipped/Failed 7 Refund(orderId) (compensate) 8 CancelOrder (compensate) 9
课堂讲授:何时不用这些模式
过度设计的信号
- 为了用而用:不是因为业务需求,而是因为"看起来高级"
- 复杂度上升但收益不明显:引入模式后,代码复杂度上升,但业务价值没有提升
- 团队不理解:团队对模式不熟悉,导致维护困难
渐进式采用
课堂建议:
- 先做简单方案:能用简单方案解决,就不要用复杂模式
- 遇到痛点再引入:例如读写性能差异大时再考虑 CQRS
- 小范围试点:先在一个 BC 或一个用例中试点,验证效果后再推广
常见误区
- 误区 1:所有项目都用 CQRS/ES:这些模式有成本,应该根据需求选择
- 误区 2:CQRS = 两个数据库:CQRS 是逻辑分离,不一定要物理分离
- 误区 3:Event Sourcing = 领域事件:Event Sourcing 是持久化策略,领域事件是通信机制
- 误区 4:Saga = 分布式事务:Saga 是最终一致性,不是强一致性
课堂练习
- 给出一个你认为"适合用 CQRS"的场景,并说明理由。
- 给出一个你认为"不适合用 Event Sourcing"的场景,并说明理由。
- 设计一个 Saga 流程(文字描述即可):假设"订单 → 支付 → 发货 → 通知"流程,请写出每个步骤的补偿动作。
自测题
- CQRS 的核心思想是什么?何时用/何时不用?
- Event Sourcing 的核心思想是什么?何时用/何时不用?
- Saga 的核心思想是什么?何时用/何时不用?
延伸阅读
- 参考资料索引:
references.md - Martin Fowler:CQRS、Event Sourcing(见 references.md)
选读说明:本章内容属于高级主题,适合有一定 DDD 基础的同学深入学习。初学者可以先跳过,掌握基础模式后再回来学习。