开篇:为什么我们需要事件驱动?
想象一下,你在网上下了一个订单。传统的系统会这样处理:
订单状态:已创建 → 已支付 → 已发货 → 已完成
每次状态变更,数据库里的 order_status 字段就被覆盖一次。看起来很合理对吧?但有一天,客服打来电话:"这个订单为什么从已支付直接变成已完成,中间的发货记录去哪了?"你傻眼了,因为数据已经被覆盖,历史消失了。
这就是传统"状态存储"模式的痛点:我们只知道现在是什么样,却不知道是怎么变成这样的。
事件驱动架构(Event-Driven Architecture)换了一个思路:不存储状态,存储事件。就像银行流水账,每一笔交易都清清楚楚,最终余额是所有交易的累加结果。
一、事件驱动架构的核心理念
1.1 什么是事件?
事件是已经发生的事实,用过去式描述,不可篡改。
传统方式(状态):
Order order = new Order();
order.setStatus("PAID"); // 我现在是"已支付"状态
事件方式(事实):
OrderPaidEvent event = new OrderPaidEvent(
orderId: "123",
amount: 299.0,
paidAt: "2025-10-16 10:30:00"
); // "订单在某个时间点被支付了" - 这是历史事实
1.2 事件驱动的三大优势
1. 完整的审计日志
-
每个状态变化都有迹可循
-
金融、医疗等强合规行业的刚需
2. 时间旅行(Time Travel)
-
可以重放历史事件,回到任意时间点的状态
-
出 Bug 时可以精确复现:"10月1日的订单数据是怎么算出来的?"
3. 解耦和可扩展
-
发送事件的人不关心谁消费
-
新业务接入只需订阅事件,不改老代码
二、Event Sourcing:用事件重建世界
2.1 核心思想
Event Sourcing(事件溯源)的核心:不存当前状态,存所有历史事件,状态通过回放事件计算出来。
类比:Git 的 commit 历史就是典型的 Event Sourcing,代码的当前状态是所有 commit 的累积结果。
2.2 实战:订单系统的事件溯源
传统方式的表结构:
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
status VARCHAR(20), -- 只保存最新状态
total_amount DECIMAL(10,2)
);
Event Sourcing 方式:
CREATE TABLE order_events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
aggregate_id VARCHAR(50), -- 订单ID
event_type VARCHAR(100), -- 事件类型
event_data JSON, -- 事件内容
version INT, -- 版本号(防并发)
created_at TIMESTAMP
);
核心代码:聚合根(Aggregate Root)
public class Order {
private String orderId;
private OrderStatus status;
private BigDecimal totalAmount;
private List<DomainEvent> uncommittedEvents = new ArrayList<>();
// 通过事件重建订单状态
public static Order rebuild(List<DomainEvent> events) {
Order order = new Order();
for (DomainEvent event : events) {
order.apply(event); // 逐个回放事件
}
return order;
}
// 业务方法:支付订单
public void pay(BigDecimal amount) {
if (this.status != OrderStatus.CREATED) {
throw new IllegalStateException("订单状态不允许支付");
}
// 产生事件,而不是直接改状态
OrderPaidEvent event = new OrderPaidEvent(orderId, amount, Instant.now());
apply(event); // 应用到内存
uncommittedEvents.add(event); // 待持久化
}
// 应用事件到当前状态(状态机)
private void apply(DomainEvent event) {
if (event instanceof OrderCreatedEvent) {
OrderCreatedEvent e = (OrderCreatedEvent) event;
this.orderId = e.getOrderId();
this.status = OrderStatus.CREATED;
this.totalAmount = e.getAmount();
} else if (event instanceof OrderPaidEvent) {
this.status = OrderStatus.PAID;
} else if (event instanceof OrderShippedEvent) {
this.status = OrderStatus.SHIPPED;
}
// ... 更多事件处理
}
}
持久化事件(Repository 层):
public class OrderRepository {
public void save(Order order) {
List<DomainEvent> events = order.getUncommittedEvents();
for (DomainEvent event : events) {
// 插入事件表
jdbcTemplate.update(
"INSERT INTO order_events (aggregate_id, event_type, event_data, version) VALUES (?, ?, ?, ?)",
order.getOrderId(),
event.getClass().getName(),
toJson(event),
order.getVersion()
);
}
// 发布到消息队列,供其他服务消费
eventBus.publish(events);
order.clearUncommittedEvents();
}
public Order findById(String orderId) {
// 查询该订单的所有历史事件
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
"SELECT * FROM order_events WHERE aggregate_id = ? ORDER BY version",
orderId
);
List<DomainEvent> events = rows.stream()
.map(this::toEvent)
.collect(Collectors.toList());
return Order.rebuild(events); // 通过事件重建订单
}
}
2.3 快照优化:别每次都回放所有事件
想象一个订单有 10000 条事件,每次查询都回放 10000 次?性能爆炸!
解决方案:快照(Snapshot)
-
每隔 N 个事件(如 100 个)保存一次完整状态
-
查询时:加载最近的快照 + 之后的增量事件
public Order findById(String orderId) {
// 1. 查询最新快照
OrderSnapshot snapshot = snapshotRepository.findLatest(orderId);
Order order = snapshot != null ? snapshot.toOrder() : new Order();
// 2. 只回放快照之后的事件
int fromVersion = snapshot != null ? snapshot.getVersion() : 0;
List<DomainEvent> events = eventRepository.findByIdAndVersionGreaterThan(orderId, fromVersion);
for (DomainEvent event : events) {
order.apply(event);
}
return order;
}
2.4 Event Sourcing 的挑战
|
挑战 |
解决方案 |
|
查询复杂 |
引入 CQRS,建立专门的查询模型 |
|
数据迁移 |
通过"升级事件"(Upcasting)处理历史事件 |
|
事件版本演进 |
事件携带版本号,兼容多版本 |
|
删除数据困难 |
GDPR 等隐私法规可用"遗忘事件"标记 |
三、CQRS:读写分离的终极形态
3.1 什么是 CQRS?
CQRS(Command Query Responsibility Segregation,命令查询职责分离):读和写用不同的模型。
传统方式:
// 同一个 Order 对象既用来写,也用来读
Order order = orderService.getById(123); // 查询
order.setStatus("PAID"); // 修改
orderService.update(order); // 保存
CQRS 方式:
// 写模型:聚合根,只处理命令
OrderAggregate order = commandService.load(123);
order.pay(amount);
commandService.save(order);
// 读模型:专门为查询优化的视图
OrderQueryView view = queryService.getOrderDetail(123); // 快速查询
3.2 为什么需要 CQRS?
痛点 1:读写需求矛盾
-
写操作:需要强一致性、事务、复杂业务逻辑
-
读操作:需要高性能、多维度查询、统计分析
一个模型很难同时满足两者。
痛点 2:复杂查询拖累性能
// 传统 ORM 的"对象-关系阻抗"
@Entity
public class Order {
@OneToMany
private List<OrderItem> items; // 每次查询都关联查询?
@ManyToOne
private Customer customer; // N+1 问题
}
CQRS 的解法:
-
写模型:规范化存储(Event Sourcing 事件表)
-
读模型:反规范化存储(宽表、ES、Redis)
3.3 实战:CQRS 架构设计
整体架构:
命令端(写) 查询端(读)
┌─────────────┐ ┌──────────────┐
│ Command API │──写──>│事件表│──事件──>│ Projection │──>│查询数据库│
└─────────────┘ └──────────────┘
│ │
└──────────────────────────────┘
异步同步
命令端(Command Side):
// 命令对象(携带意图)
public class PayOrderCommand {
private String orderId;
private BigDecimal amount;
private String paymentMethod;
}
// 命令处理器
@Service
public class OrderCommandHandler {
@Transactional
public void handle(PayOrderCommand command) {
// 1. 加载聚合根
Order order = orderRepository.findById(command.getOrderId());
// 2. 执行业务逻辑
order.pay(command.getAmount());
// 3. 保存事件
orderRepository.save(order); // 写入 order_events 表
}
}
查询端(Query Side):
// 查询视图(宽表设计)
@Entity
@Table(name = "order_query_view")
public class OrderQueryView {
private String orderId;
private String customerName; // 冗余客户信息
private String productNames; // 冗余商品名称
private BigDecimal totalAmount;
private String statusText; // 状态的可读文本
private LocalDateTime createdAt;
}
// 投影器(Projector):消费事件,更新查询视图
@Component
public class OrderQueryProjector {
@EventListener
public void on(OrderPaidEvent event) {
// 更新查询视图
OrderQueryView view = queryRepository.findById(event.getOrderId());
view.setStatusText("已支付");
view.setPaidAt(event.getPaidAt());
queryRepository.save(view);
}
@EventListener
public void on(OrderShippedEvent event) {
OrderQueryView view = queryRepository.findById(event.getOrderId());
view.setStatusText("已发货");
view.setShippedAt(event.getShippedAt());
queryRepository.save(view);
}
}
3.4 最终一致性的处理
CQRS 的代价是:命令端和查询端数据不同步。
场景:用户刚支付完,刷新页面还显示"待支付"
解决方案:
1. 前端轮询(简单粗暴)
// 支付成功后,每隔 500ms 查询一次状态,最多 10 次
for (let i = 0; i < 10; i++) {
await sleep(500);
const order = await api.getOrder(orderId);
if (order.status === 'PAID') break;
}
2. 命令返回预期状态(乐观 UI)
public CommandResult handle(PayOrderCommand command) {
// ... 执行命令
return CommandResult.builder()
.success(true)
.expectedStatus("PAID") // 告诉前端预期状态
.eventId(event.getId()) // 前端可用此 ID 对账
.build();
}
3. WebSocket 推送(实时性要求高)
@EventListener
public void on(OrderPaidEvent event) {
// 投影更新完成后,推送给前端
webSocketService.send(event.getOrderId(), "状态已更新");
}
4. 版本号机制
public class OrderQueryView {
private Long version; // 版本号,与命令端的事件版本对应
}
// 查询时验证版本
public OrderQueryView getOrder(String orderId, Long minVersion) {
OrderQueryView view = queryRepository.findById(orderId);
if (view.getVersion() < minVersion) {
// 投影还没更新到最新,等待或返回提示
throw new ViewNotReadyException("数据同步中...");
}
return view;
}
四、实战:电商系统的完整落地
4.1 领域模型设计
核心聚合根:
-
Order(订单)
-
Inventory(库存)
-
Payment(支付)
事件流:
1. 用户下单
└─> OrderCreatedEvent
├─> 扣减库存(InventoryReservedEvent)
└─> 创建支付单(PaymentCreatedEvent)
2. 用户支付
└─> PaymentCompletedEvent
└─> 订单确认(OrderConfirmedEvent)
3. 商家发货
└─> OrderShippedEvent
4.2 事件设计的最佳实践
1. 事件命名:领域语言 + 过去式
// ❌ 糟糕的命名
UpdateOrderStatusEvent
// ✅ 好的命名
OrderPaidEvent
OrderShippedEvent
OrderCancelledEvent
2. 事件内容:包含完整上下文
public class OrderPaidEvent {
private String orderId;
private BigDecimal amount;
private String paymentMethod; // 支付方式
private String userId;
private Instant paidAt;
private String eventId; // 幂等性保证
}
3. 事件版本化
// V1 版本
public class OrderCreatedEventV1 {
private String orderId;
private BigDecimal amount;
}
// V2 版本(新增字段)
public class OrderCreatedEventV2 {
private String orderId;
private BigDecimal amount;
private String currency; // 新增:货币类型
// 提供升级方法
public static OrderCreatedEventV2 upgrade(OrderCreatedEventV1 v1) {
return new OrderCreatedEventV2(v1.getOrderId(), v1.getAmount(), "CNY");
}
}
4.3 Saga 模式:分布式事务的事件编排
场景:下单流程跨多个服务
订单服务 → 库存服务 → 支付服务
如果支付失败,需要回滚库存和订单。
Saga 模式:通过事件串联多个本地事务
// 编排器(Saga Orchestrator)
@Component
public class CreateOrderSaga {
public void start(CreateOrderCommand command) {
// Step 1: 创建订单
OrderCreatedEvent orderEvent = orderService.createOrder(command);
try {
// Step 2: 扣减库存
InventoryReservedEvent inventoryEvent = inventoryService.reserve(orderEvent);
// Step 3: 创建支付
PaymentCreatedEvent paymentEvent = paymentService.create(inventoryEvent);
// Step 4: 完成订单
orderService.confirm(orderEvent.getOrderId());
} catch (Exception e) {
// 补偿事务(Compensating Transaction)
inventoryService.release(orderEvent.getOrderId()); // 释放库存
orderService.cancel(orderEvent.getOrderId()); // 取消订单
}
}
}
五、技术选型与工具链
5.1 事件存储(Event Store)
|
方案 |
优点 |
缺点 |
适用场景 |
|
MySQL |
简单、事务支持 |
性能有限 |
中小型系统 |
|
EventStoreDB |
专为 ES 设计 |
学习成本高 |
Event Sourcing 重度使用 |
|
Kafka |
高吞吐、持久化 |
不支持随机查询 |
事件流转 |
|
Cassandra |
海量数据 |
一致性较弱 |
超大规模 |
推荐组合:MySQL(事件存储)+ Kafka(事件分发)
5.2 开源框架
1. Axon Framework(Java)
-
集成了 Event Sourcing + CQRS
-
内置 Saga 支持
-
社区活跃
2. EventFlow(.NET)
-
轻量级
-
适合快速上手
3. 自研(灵活性高)
-
适合有特殊业务逻辑的团队
-
上面的示例代码就是自研的简化版
六、踩坑与避坑指南
6.1 不是所有场景都适合 Event Sourcing
❌ 不适合的场景:
-
CRUD 为主的管理后台
-
业务逻辑简单、不需要审计
-
团队对事件驱动理解不足
✅ 适合的场景:
-
金融交易(每笔记录都要追溯)
-
物流追踪(状态变化频繁)
-
协同编辑(如 Google Docs)
6.2 事件风暴(Event Storming)必不可少
什么是事件风暴?
-
团队一起用便利贴贴墙,梳理业务流程中的所有事件
-
识别领域边界、聚合根、命令
为什么重要?
-
Event Sourcing 的成败 80% 取决于事件设计
-
事件设计错了,后期重构成本巨大
6.3 监控告警不能少
关键指标:
-
事件延迟:命令端事件产生 → 查询端投影完成的时间
-
投影失败率:消费事件时的异常比例
-
事件回放耗时:系统恢复时需要回放多少事件
@Component
public class EventMetrics {
@Timed("event.projection.duration")
public void project(DomainEvent event) {
try {
// 投影逻辑
} catch (Exception e) {
meterRegistry.counter("event.projection.failed").increment();
throw e;
}
}
}
七、进阶话题
7.1 事件溯源 + 区块链
事件不可篡改的特性天然适合区块链:
-
每个事件作为一个区块
-
通过哈希链保证数据完整性
-
适用于多方协作、需要公信力的场景(供应链金融)
7.2 实时物化视图(Materialized View)
通过 Flink/Spark Streaming 实时消费事件,生成复杂的聚合视图:
// Flink 示例(伪代码)
events.keyBy(OrderEvent::getOrderId)
.timeWindow(Time.days(1))
.aggregate(new OrderStatisticsAggregator())
.addSink(new ElasticsearchSink()); // 写入 ES 供查询
7.3 事件驱动的微服务通信
传统 RPC 调用:
订单服务 --RPC--> 库存服务 --RPC--> 支付服务
强耦合,任何一个服务挂了,整个流程卡住。
事件驱动:
订单服务 --发布事件--> 消息队列 <--订阅-- 库存服务、支付服务
各服务独立消费,解耦且高可用。
结语:从理念到落地的思维跃迁
事件驱动架构不只是技术选型,更是思维方式的转变:
-
从"当前是什么"到"发生了什么"
-
从"数据修改"到"事实追加"
-
从"同步调用"到"异步响应"
小步快跑的落地路径:
-
Phase 1:引入事件总线
-
在现有系统中加入事件发布/订阅,服务间解耦
-
工具:Spring Event、Guava EventBus
-
-
Phase 2:关键聚合引入 Event Sourcing
-
选择核心领域(如订单、账户)试点
-
保留传统表作为快照/查询视图
-
-
Phase 3:全面 CQRS
-
读写彻底分离
-
引入专业的 Event Store
-
记住:架构演进是渐进式的,不要一上来就推倒重来!

197

被折叠的 条评论
为什么被折叠?



