Event Sourcing + CQRS 深度落地实战:从概念到生产级实现

Event Sourcing + CQRS 深度落地实战:从概念到生产级实现

很多团队把 CQRS 理解成"读写分离",把 Event Sourcing 理解成"存操作日志"。这两个理解都差了一层。本文从事件溯源的本质出发,结合 Spring Boot + Axon Framework 的生产级实现,带你走完一个真实订单系统从传统架构迁移到 ES+CQRS 的完整过程。


一、CQRS 不是读写分离,ES 不是操作日志

CQRS 的本质

CQRS(Command Query Responsibility Segregation,命令查询职责分离)的核心思想是:

修改状态的操作(Command)和读取状态的操作(Query)使用不同的模型。

复制代码
传统 CRUD 架构:
  Service → Repository → 同一个数据库(既读又写)

CQRS 架构:
  Command Side:CommandHandler → 写模型 → 写库(事务强一致)
  Query Side:QueryHandler → 读模型 → 读库(最终一致,高度优化)

关键价值:

  • 写模型聚焦业务规则与状态变更,不受查询需求干扰
  • 读模型可以针对不同查询场景独立优化(宽表、Elasticsearch、Redis)
  • 读写分别扩展,高读负载场景读副本横向扩容

Event Sourcing 的本质

传统系统存储的是当前状态 (最新快照):

订单表:order_id=1001, status=SHIPPED, amount=299.00

Event Sourcing 存储的是状态变更的历史事件序列

复制代码
事件1: OrderCreated    { orderId: 1001, userId: 501, amount: 299 }
事件2: PaymentReceived { orderId: 1001, paymentId: P888, paidAt: ... }
事件3: OrderShipped    { orderId: 1001, trackingNo: SF123, shippedAt: ... }

当前状态 = 从初始状态回放所有事件的结果。

核心价值:

  • 完整的业务操作历史,天然的审计日志
  • 任意时间点的状态回溯(时间旅行)
  • 事件是领域通用语言,代码与业务高度对齐
  • 事件可供下游系统订阅,天然的集成总线

二、ES + CQRS 架构全貌

复制代码
┌──────────────────────────────────────────────────────────────────┐
│                         客户端                                    │
└──────────────┬───────────────────────────────────┬──────────────┘
               │ Command                           │ Query
               ▼                                   ▼
┌──────────────────────┐              ┌──────────────────────────┐
│    Command Gateway   │              │      Query Gateway        │
└──────────┬───────────┘              └────────────┬─────────────┘
           │                                       │
           ▼                                       ▼
┌──────────────────────┐              ┌──────────────────────────┐
│  Command Handler     │              │      Query Handler        │
│  (聚合根业务逻辑)      │              │   (从读库直接查询)         │
└──────────┬───────────┘              └────────────┬─────────────┘
           │ 产生 Domain Event                      │
           ▼                                       │
┌──────────────────────┐              ┌────────────▼─────────────┐
│    Event Store       │─── Projector ─→   Read Store            │
│  (事件永久存储)        │   (事件投影)   │  (MySQL/ES/Redis)       │
│  EventStoreDB/Kafka  │              └──────────────────────────┘
└──────────────────────┘

三、生产级实现:Axon Framework + Spring Boot

3.1 项目依赖

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.axonframework</groupId>
    <artifactId>axon-spring-boot-starter</artifactId>
    <version>4.9.3</version>
</dependency>
<dependency>
    <groupId>org.axonframework.extensions.kafka</groupId>
    <artifactId>axon-kafka-spring-boot-starter</artifactId>
    <version>4.0.0</version>
</dependency>

3.2 定义领域事件(Events)

java 复制代码
// 事件是不可变的值对象,代表已发生的事实
public record OrderCreatedEvent(
    String orderId,
    String userId,
    List<OrderItem> items,
    BigDecimal totalAmount,
    LocalDateTime createdAt
) {}

public record OrderPaidEvent(
    String orderId,
    String paymentId,
    BigDecimal paidAmount,
    LocalDateTime paidAt
) {}

public record OrderShippedEvent(
    String orderId,
    String trackingNumber,
    String carrier,
    LocalDateTime shippedAt
) {}

public record OrderCancelledEvent(
    String orderId,
    String reason,
    LocalDateTime cancelledAt
) {}

3.3 聚合根(Command Side)

java 复制代码
@Aggregate
public class OrderAggregate {

    @AggregateIdentifier
    private String orderId;
    private OrderStatus status;
    private BigDecimal totalAmount;

    // 空构造器(Axon 反射使用)
    protected OrderAggregate() {}

    // 处理创建命令
    @CommandHandler
    public OrderAggregate(CreateOrderCommand command) {
        // 1. 业务规则校验
        if (command.items().isEmpty()) {
            throw new IllegalArgumentException("订单商品不能为空");
        }
        if (command.totalAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("订单金额必须大于0");
        }

        // 2. 发布领域事件(注意:不直接修改状态!)
        AggregateLifecycle.apply(new OrderCreatedEvent(
            command.orderId(),
            command.userId(),
            command.items(),
            command.totalAmount(),
            LocalDateTime.now()
        ));
    }

    // 处理支付命令
    @CommandHandler
    public void handle(PayOrderCommand command) {
        if (this.status != OrderStatus.CREATED) {
            throw new OrderStateException("只有待支付订单才能发起支付,当前状态:" + this.status);
        }
        AggregateLifecycle.apply(new OrderPaidEvent(
            this.orderId,
            command.paymentId(),
            command.paidAmount(),
            LocalDateTime.now()
        ));
    }

    // 处理发货命令
    @CommandHandler
    public void handle(ShipOrderCommand command) {
        if (this.status != OrderStatus.PAID) {
            throw new OrderStateException("只有已支付订单才能发货");
        }
        AggregateLifecycle.apply(new OrderShippedEvent(
            this.orderId,
            command.trackingNumber(),
            command.carrier(),
            LocalDateTime.now()
        ));
    }

    // 事件处理器:根据事件更新聚合状态(这里才修改内存状态)
    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.orderId();
        this.status = OrderStatus.CREATED;
        this.totalAmount = event.totalAmount();
    }

    @EventSourcingHandler
    public void on(OrderPaidEvent event) {
        this.status = OrderStatus.PAID;
    }

    @EventSourcingHandler
    public void on(OrderShippedEvent event) {
        this.status = OrderStatus.SHIPPED;
    }
}

3.4 事件投影(Projector - 构建读模型)

java 复制代码
@Component
@ProcessingGroup("order-projection")
public class OrderProjector {

    @Autowired
    private OrderReadRepository readRepository;

    @Autowired
    private ElasticsearchClient esClient;

    // 监听 OrderCreated 事件,更新 MySQL 读模型
    @EventHandler
    public void on(OrderCreatedEvent event) {
        OrderReadModel model = new OrderReadModel();
        model.setOrderId(event.orderId());
        model.setUserId(event.userId());
        model.setStatus("CREATED");
        model.setTotalAmount(event.totalAmount());
        model.setCreatedAt(event.createdAt());
        readRepository.save(model);

        // 同步到 ES,支持全文搜索
        indexToElasticsearch(model);
    }

    @EventHandler
    public void on(OrderPaidEvent event) {
        readRepository.findByOrderId(event.orderId()).ifPresent(model -> {
            model.setStatus("PAID");
            model.setPaidAt(event.paidAt());
            readRepository.save(model);
        });
    }

    @EventHandler
    public void on(OrderShippedEvent event) {
        readRepository.findByOrderId(event.orderId()).ifPresent(model -> {
            model.setStatus("SHIPPED");
            model.setTrackingNumber(event.trackingNumber());
            model.setShippedAt(event.shippedAt());
            readRepository.save(model);
        });
    }

    // 投影失败处理:记录错误,支持重试
    @ExceptionHandler(resultType = Exception.class)
    public void handle(Exception ex, EventMessage<?> event) {
        log.error("订单投影处理失败,eventId: {}, error: {}", 
            event.getIdentifier(), ex.getMessage());
        // 发送告警,不抛出异常(避免阻塞后续事件)
        alertService.sendAlert("OrderProjector", event.getIdentifier(), ex);
    }
}

3.5 Saga:跨聚合分布式业务流程

java 复制代码
// Saga 处理跨聚合的长事务(订单支付后自动触发库存扣减)
@Saga
public class OrderFulfillmentSaga {

    @Autowired
    private transient CommandGateway commandGateway;

    private String orderId;
    private String inventoryReservationId;

    // Saga 启动:订单支付成功后
    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderPaidEvent event) {
        this.orderId = event.orderId();

        // 发送库存预占命令
        String reservationId = UUID.randomUUID().toString();
        this.inventoryReservationId = reservationId;

        commandGateway.send(new ReserveInventoryCommand(
            reservationId,
            event.orderId(),
            items  // 从事件中获取商品列表
        ));
    }

    // 库存预占成功,触发发货
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(InventoryReservedEvent event) {
        commandGateway.send(new ArrangeShipmentCommand(event.orderId()));
    }

    // 库存预占失败,回滚订单
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(InventoryReservationFailedEvent event) {
        commandGateway.send(new CancelOrderCommand(
            event.orderId(), 
            "库存不足:" + event.reason()
        ));
    }

    @EndSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderShippedEvent event) {
        // Saga 结束
        log.info("订单履约流程完成,orderId: {}", event.orderId());
    }
}

四、事件快照:解决事件积累的性能问题

当一个聚合的事件数量超过 1000 条时,每次加载需要回放大量事件,性能会下降。

解决方案:定期创建快照

java 复制代码
@Component
public class OrderSnapshotTrigger {

    @Autowired
    private Snapshotter snapshotter;

    // 每 50 个事件自动触发快照
    @EventHandler
    public void triggerSnapshot(OrderShippedEvent event, 
                                 @SequenceNumber long seqNo) {
        if (seqNo % 50 == 0) {
            snapshotter.scheduleSnapshot(OrderAggregate.class, event.orderId());
        }
    }
}

// Axon 配置快照策略
@Configuration
public class AxonConfig {

    @Bean
    public SnapshotTriggerDefinition orderSnapshotTrigger(Snapshotter snapshotter) {
        // 每100个事件自动创建快照
        return new EventCountSnapshotTriggerDefinition(snapshotter, 100);
    }
}

五、企业落地踩坑总结

坑1:事件设计过于细粒度

错误: 把每次字段更新都设计成单独事件(UserNameUpdatedEvent, UserEmailUpdatedEvent

正确: 按业务语义设计事件(UserProfileUpdatedEvent),一个业务操作一个事件

坑2:事件中包含可变引用

错误: 事件对象包含 List<OrderItem>OrderItem 是可变类

正确: 事件对象全部使用不可变类型(Record、ImmutableList),事件一旦持久化不可变

坑3:投影跟不上事件消费速度

症状: 读库数据严重滞后,用户刚下单页面查不到

解法:

  • 投影器做水平扩展(多实例并行消费不同分区)
  • 对用户感知强的数据(如自己的订单)在 Command Side 写完后同步返回,不依赖最终一致

坑4:事件格式版本演进

场景: 半年后 OrderCreatedEvent 需要加新字段

解法:

java 复制代码
// 使用 Upcaster 做事件版本升级
@Component
public class OrderCreatedEventUpcaster 
    implements SingleEventUpcaster {

    @Override
    public boolean canUpcast(IntermediateEventRepresentation ir) {
        return "OrderCreatedEvent".equals(ir.getType().getName())
            && ir.getType().getRevision().equals(Optional.of("1.0"));
    }

    @Override
    public IntermediateEventRepresentation doUpcast(
            IntermediateEventRepresentation ir) {
        // 为旧事件添加新字段默认值
        return ir.upcastPayload(
            new MetaData(Collections.singletonMap("channelId", "UNKNOWN")),
            XStreamXmlMessageConverter.class,
            document -> {
                document.getRootElement()
                    .addElement("channelId").setText("UNKNOWN");
                return document;
            }
        );
    }
}

六、全文总结

ES + CQRS 不是银弹,适用场景:

  • 业务流程复杂、状态变更多、审计需求强(金融、电商、保险)
  • 读写负载差异悬殊,需要独立扩展
  • 需要完整事件历史用于业务分析或 BI

不适用场景:

  • 简单 CRUD 系统(引入 ES+CQRS 是过度工程)
  • 团队对事件建模不熟悉(学习成本高,需要专项培训)

引入路径建议: 先在最核心、最复杂的单一聚合上试点(如订单),跑通工程体系后再推广。


七、行业技术展望

  • Axon Server 作为专用事件存储和消息路由器,正在替代 Kafka 在 ES 场景的角色
  • EventStoreDB 作为专门的事件存储数据库进入更多企业视野
  • 结合 AI 的事件流分析:基于事件溯源的数据反哺大模型训练正在成为新的架构方向
  • Temporal.io 将工作流引擎与事件溯源结合,正在替代传统 Saga 实现

参考文献

  1. Martin Fowler - Event Sourcing Pattern - https://martinfowler.com/eaaDev/EventSourcing.html
  2. Martin Fowler - CQRS - https://martinfowler.com/bliki/CQRS.html
  3. Axon Framework 官方文档 - https://docs.axoniq.io/reference-guide/
  4. Greg Young - CQRS Documents - https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
  5. EventStoreDB 官方文档 - https://developers.eventstore.com/
  6. Microsoft Azure Architecture Center - CQRS Pattern - https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs
  7. 《领域驱动设计》Eric Evans 著(原版)
  8. Vaughn Vernon《实现领域驱动设计》- 机械工业出版社
  9. 阿里云微服务架构最佳实践 - https://help.aliyun.com/zh/mse/
相关推荐
hey you~5 小时前
呼叫中心系统技术选型全景指南:从CTI架构到云原生演进的技术决策框架
系统架构·云计算·技术选型·呼叫中心·cti
半夏映浮光1 天前
系统架构设计师知识点81-100
系统架构
lkshop1 天前
生成式引擎优化(GEO)系统架构:从元数据采集到多源一致性校验
系统架构·geo
大蚂蚁2号1 天前
Fuck Stop code: VIDEO_DXGKRNL_FATAL_ERROR (0x113)
windows·系统架构
跨境数据猎手1 天前
反向海淘SaaS系统架构拆解与业务技术
大数据·爬虫·系统架构
凯云-金山HIL1 天前
打破仿真孤岛:基于凯云SimuRTS的分布式联合仿真系统架构深度解析
自动化测试·分布式·系统架构·etest·半实物仿真
派叔2 天前
老字号数字化转型的困境与路径:一个系统架构视角下的观察
大数据·人工智能·系统架构·产品运营·流量运营
Hardworking6662 天前
第4章 信息系统架构
系统架构·系统集成项目管理工程师·信息系统架构·信息技术服务
AI 小老六2 天前
Agent 评测系统架构:从指标分层到 GT/Judge 闭环的工程化落地
人工智能·ai·架构·系统架构·可用性测试