第 16 章 · 事件驱动架构与 CQRS 入门
本章目标:理解 事件驱动架构(EDA) 与 ch06 Spring Cloud Stream 的关系;掌握 CQRS(命令查询职责分离) 读写模型拆分;在 svc-spring-demo 设计「订单写模型 + 订单读模型」教学示例;了解 事件溯源(Event Sourcing) 与 投影(Projection) 概念;对比 CRUD 微服务与 EDA 的适用边界。
学时建议:5~6 小时(含 2 小时读模型投影实现)
前置:spring-cloud-web ch01~ch15;ch06 消息驱动;ch07 一致性讨论。
16.1 从 CRUD 微服务到 EDA
ch04~ch07 的 order-svc 典型 CRUD:
POST /orders → 写 order_db
GET /orders → 读 order_db(同一表)
随着查询变复杂(关联商品、用户、物流、统计),读路径拖慢写路径,且跨服务 JOIN 困难。
EDA + CQRS 思路:
命令侧(Write) 查询侧(Read)
order-svc 接收 CreateOrder order-query-svc 提供丰富查询
│ ▲
│ OrderCreatedEvent │ 投影更新
└──────────────► RabbitMQ ─────────────┘
| 模式 | 写 | 读 |
|---|---|---|
| CRUD | order 表 | 同表 |
| CQRS | order 命令库 | order_read 宽表 / ES |
| Event Sourcing | 事件流 | 从事件重建状态(进阶) |
16.2 领域事件回顾(ch06)
public record OrderCreatedEvent(
String eventId,
Long orderId,
Long userId,
String tenantId,
List<OrderLineEvent> lines,
Instant occurredAt,
int schemaVersion
) {}
EDA 原则:
- 事件名用过去式(
OrderCreated非CreateOrder) - 事件是不可变事实
- 消费者幂等处理(
eventId去重)
16.3 CQRS 命令侧(order-svc)
保持写路径精简,专注业务规则与一致性:
@Service
@RequiredArgsConstructor
public class CreateOrderCommandHandler {
private final OrderRepository orderRepository;
private final StreamBridge streamBridge;
@Transactional
public Long handle(CreateOrderCommand cmd) {
Order order = validateAndBuild(cmd);
orderRepository.save(order);
OrderCreatedEvent event = OrderEventMapper.toCreated(order);
// 教学简化:事务内直接发消息;生产环境请改用 16.9 的 Outbox 模式
streamBridge.send("orderCreated-out-0", MessageBuilder
.withPayload(event)
.setHeader("eventId", event.eventId())
.build());
return order.getId();
}
}
重要:上面在@Transactional内直接streamBridge.send是教学简化,存在两个一致性漏洞:① 消息发出后事务回滚 → 下游看到「幽灵事件」;② DB 提交成功但 MQ 发送失败 → 事件丢失。生产环境应使用 16.9 的 Outbox 模式(事件与订单同一本地事务落库,再由 CDC/定时任务可靠投递)。
写库只存订单聚合根必要字段;复杂报表字段不进入写模型。
16.4 CQRS 查询侧(order-query-svc)
新建 order-query-svc,自有 order_read_db(可与写库物理分离):
@Entity
@Table(name = "order_read_view")
public class OrderReadView {
@Id private Long orderId;
private Long userId;
private String status;
private BigDecimal totalAmount;
private String buyerNickname; // 投影时从 user 事件或 Feign 快照
private String productSummaryJson; // 商品摘要 JSON
private Instant createdAt;
}
投影消费者:
@Bean
public Consumer<Message<OrderCreatedEvent>> orderProjection() {
return msg -> {
OrderCreatedEvent e = msg.getPayload();
if (processedEvents.exists(e.eventId())) return;
OrderReadView view = new OrderReadView();
view.setOrderId(e.orderId());
view.setUserId(e.userId());
view.setStatus("CREATED");
view.setTotalAmount(calculateTotal(e.lines()));
view.setBuyerNickname(userSnapshotClient.getNickname(e.userId()));
view.setProductSummaryJson(buildSummary(e.lines()));
orderReadRepository.save(view);
processedEvents.mark(e.eventId());
};
}
查询 API:
@GetMapping("/api/v1/order-views/me")
public Result<Page<OrderReadViewDTO>> myOrders(Pageable pageable) {
return Result.ok(queryService.findByUser(UserContext.getUserId(), pageable));
}
Gateway 路由 /api/v1/order-views/** → order-query-svc;写仍走 order-svc。