下载工作台
Spring Cloud 微服务

事件驱动架构与 CQRS 入门

试读上半部分 · 解锁后可读全文

第 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 ─────────────┘
模式
CRUDorder 表同表
CQRSorder 命令库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 原则:

  1. 事件名用过去式OrderCreatedCreateOrder
  2. 事件是不可变事实
  3. 消费者幂等处理(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。


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。