3、事件驱动架构入门:事件、事件源、事件总线、事件驱动 vs 请求驱动
好,咱们今天聊聊事件驱动架构。说实话,这个概念在 SOA 里太重要了。我刚开始接触微服务时,满脑子都是 RESTful API,觉得服务之间就该你调我我调你。直到有一次,我在一个电商项目里被“请求驱动”坑得够呛——订单服务挂了,下游六个服务全部阻塞,整个系统像多米诺骨牌一样倒下。嗯,从那以后,我才真正开始研究事件驱动。
3.1 什么是事件?
事件,说白了就是“已经发生的事情”。在软件世界里,它是一条不可变的数据记录,描述了一个事实。比如“用户下单了”、“支付成功了”、“库存扣减了”。
我个人习惯把事件看作一种“消息”,但它比普通消息更讲究。事件里通常包含三个要素:
- 事件类型:比如
OrderCreated、PaymentCompleted - 事件时间戳:事情发生的精确时间
- 事件数据:相关的业务信息,比如订单ID、金额
核心要点:事件是事实的陈述,不是指令。它告诉别人“发生了什么”,而不是“你该做什么”。
举个例子,我在项目中写过这样的事件结构:
{
"eventType": "OrderCreated",
"timestamp": "2025-03-15T10:30:00Z",
"data": {
"orderId": "ORD-20250315-001",
"userId": "U12345",
"totalAmount": 299.00
}
}
你看,这个事件只描述了“订单已创建”这个事实。至于下游服务要不要发邮件、要不要更新库存,那是它们自己的事。
3.2 事件源:谁在产生事件?
事件源,就是产生事件的源头。在微服务架构里,每个服务都可能成为事件源。比如订单服务产生 OrderCreated,支付服务产生 PaymentCompleted。
这里有个容易踩的坑——事件源应该只发布自己领域内的事件。我曾经见过一个团队,把用户服务的事件源硬塞进了订单服务里,结果两个服务耦合得死死的,改一个就要改另一个。你想想看,这跟传统的请求驱动有什么区别?
避坑指南:我曾经在一个项目中,因为图省事,让订单服务直接发布了“用户积分变更”事件。结果用户服务改了积分规则,订单服务也得跟着改。后来我花了整整两天重构,才把事件源职责划分清楚。记住:每个服务只发布自己负责的业务事件。
3.3 事件总线:事件的“高速公路”
事件总线,你可以把它想象成一条高速公路。事件源把事件“扔”到总线上,消费者从总线上“取”走自己感兴趣的事件。它解决了两个核心问题:
- 解耦:生产者不需要知道谁在消费
- 广播:一个事件可以被多个消费者同时处理
常见的实现方式有:
| 实现方式 | 特点 | 适用场景 |
|---|---|---|
| 消息队列(如 Kafka、RabbitMQ) | 持久化、高吞吐、支持回溯 | 核心业务事件、需要保证不丢失 |
| 内存事件总线(如 Guava EventBus) | 轻量、低延迟、不支持持久化 | 单体应用内部、非关键事件 |
| 云原生事件总线(如 AWS EventBridge) | 托管服务、自动扩缩容 | 云上微服务、跨账号事件 |
我个人习惯在关键业务场景用 Kafka。为什么?因为它能保证事件不丢,而且可以回溯重放。有一次线上出了 bug,订单状态不一致,我就是靠 Kafka 的事件回溯,一条条查出来问题出在哪的。
小技巧:事件总线的设计要遵循“至少一次投递”原则。别追求“恰好一次”,那太难了。你可以在消费者端做幂等处理,这样即使事件重复投递,也不会影响业务。
3.4 事件驱动 vs 请求驱动
这两个概念,我经常被问到有什么区别。说白了,它们代表了两种完全不同的协作模式。
请求驱动:服务 A 直接调用服务 B 的接口,等待返回结果。就像你打电话问朋友“现在几点了?”,朋友告诉你答案,你才能继续。
事件驱动:服务 A 发布一个事件,然后就不管了。服务 B 监听到事件后,自己决定怎么处理。就像你在群里发了一条消息“我饿了”,有人看到后可能会回复“一起去吃饭”,也可能没人理你——你不需要等谁。
我画个对比表,你一看就明白:
| 维度 | 请求驱动 | 事件驱动 |
|---|---|---|
| 耦合度 | 高(调用方依赖被调用方) | 低(双方只依赖事件格式) |
| 响应时间 | 同步等待,可能阻塞 | 异步非阻塞,响应更快 |
| 容错性 | 下游挂了,上游也受影响 | 下游挂了,事件暂存,恢复后继续处理 |
| 可扩展性 | 需要修改调用链 | 新增消费者只需订阅事件 |
| 数据一致性 | 强一致性(通过事务) | 最终一致性(通过补偿机制) |
你想想看,在微服务架构里,服务数量一多,请求驱动的调用链会变得像蜘蛛网一样复杂。而事件驱动就像一条条独立的流水线,每个服务只管自己的事。
我的建议:别把事件驱动当成万能药。对于需要实时返回结果的场景(比如用户登录验证),请求驱动更合适。对于可以异步处理的场景(比如发送通知、更新缓存),事件驱动是更好的选择。我在项目中通常混合使用——核心链路用请求驱动,非核心链路用事件驱动。
3.5 一个简单的例子
咱们来看一个实际的代码片段。假设订单服务创建订单后,需要通知库存服务和通知服务:
// 事件源:订单服务
public class OrderService {
private final EventBus eventBus;
public Order createOrder(OrderRequest request) {
Order order = saveOrder(request);
// 发布事件,不关心谁消费
eventBus.publish(new OrderCreatedEvent(order.getId(), order.getUserId()));
return order;
}
}
// 消费者:库存服务
public class InventoryService {
@Subscribe
public void handleOrderCreated(OrderCreatedEvent event) {
// 扣减库存
inventoryRepository.deduct(event.getOrderId());
}
}
// 消费者:通知服务
public class NotificationService {
@Subscribe
public void handleOrderCreated(OrderCreatedEvent event) {
// 发送通知
notificationClient.send(event.getUserId(), "您的订单已创建");
}
}
你看,订单服务只发布了事件,库存服务和通知服务各自处理。如果将来要增加一个“积分服务”,只需要再写一个消费者订阅 OrderCreatedEvent 就行,订单服务完全不用改。
经验之谈:事件的定义要尽量稳定。我建议用 Protobuf 或 Avro 来定义事件 schema,这样即使字段有变化,也能保证向前兼容。我曾经因为用 JSON 定义事件,字段名改了一个字母,结果所有消费者都报错——那场面,真是惨不忍睹。
好了,这一章就到这里。事件驱动架构的核心就是“事件”、“事件源”、“事件总线”这三个概念。下一章咱们会深入聊聊事件驱动的进阶话题——事件溯源和 CQRS。到时候我会分享一个我踩过的坑,保证让你印象深刻。