3、事件驱动架构入门:事件、事件源、事件总线、事件驱动 vs 请求驱动

好,咱们今天聊聊事件驱动架构。说实话,这个概念在 SOA 里太重要了。我刚开始接触微服务时,满脑子都是 RESTful API,觉得服务之间就该你调我我调你。直到有一次,我在一个电商项目里被“请求驱动”坑得够呛——订单服务挂了,下游六个服务全部阻塞,整个系统像多米诺骨牌一样倒下。嗯,从那以后,我才真正开始研究事件驱动。

3.1 什么是事件?

事件,说白了就是“已经发生的事情”。在软件世界里,它是一条不可变的数据记录,描述了一个事实。比如“用户下单了”、“支付成功了”、“库存扣减了”。

我个人习惯把事件看作一种“消息”,但它比普通消息更讲究。事件里通常包含三个要素:

  • 事件类型:比如 OrderCreatedPaymentCompleted
  • 事件时间戳:事情发生的精确时间
  • 事件数据:相关的业务信息,比如订单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。到时候我会分享一个我踩过的坑,保证让你印象深刻。