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 实现
参考文献
- Martin Fowler - Event Sourcing Pattern - https://martinfowler.com/eaaDev/EventSourcing.html
- Martin Fowler - CQRS - https://martinfowler.com/bliki/CQRS.html
- Axon Framework 官方文档 - https://docs.axoniq.io/reference-guide/
- Greg Young - CQRS Documents - https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
- EventStoreDB 官方文档 - https://developers.eventstore.com/
- Microsoft Azure Architecture Center - CQRS Pattern - https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs
- 《领域驱动设计》Eric Evans 著(原版)
- Vaughn Vernon《实现领域驱动设计》- 机械工业出版社
- 阿里云微服务架构最佳实践 - https://help.aliyun.com/zh/mse/