事件驱动架构:让系统学会“察言观色”

说实话,事件驱动架构是我在实际项目中用得最多、也最“上头”的一种架构风格。为什么?因为它太符合现实世界的运作方式了——你不需要主动去问别人“你好了没”,而是等别人告诉你“我好了”。

我记得刚入行那会儿,做的第一个企业级系统就是典型的“轮询地狱”。每隔几秒去数据库查一次订单状态,CPU 空转,数据库压力大,用户体验还差。后来重构成了事件驱动,整个世界清静了。

好,咱们今天就把事件驱动架构彻底聊透。

什么是事件驱动架构?

事件驱动架构(Event-Driven Architecture,EDA),说白了就是:系统通过产生、检测、消费事件来通信和协作

你想想看,传统的请求-响应模式就像你打电话问朋友“饭吃了吗?”——你得等着他回答。而事件驱动就像你发了一条朋友圈“我开饭了”,朋友们看到了自然会点赞或评论,你不需要一个个去通知。

核心思想就一句话:生产者不关心谁消费,消费者不关心谁生产。两者通过事件解耦。

关键定义:事件是“已经发生的事情”的记录,是不可变的。事件驱动架构中,组件之间不直接调用,而是通过事件总线或消息队列进行异步通信。

核心组件:三驾马车

事件驱动架构有三个核心组件,缺一不可。我习惯把它们比作“快递系统”:

  • 事件(Event)——就是快递包裹本身
  • 事件处理器(Event Handler)——就是收快递的人
  • 事件通道(Event Channel)——就是快递运输网络

1. 事件(Event)

事件是架构的“血液”。它记录的是“发生了什么”,而不是“要做什么”。

举个例子:

// 好的事件命名(过去时)
{
  "eventType": "OrderPlaced",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "orderId": "ORD-20250115-001",
    "customerId": "CUST-888",
    "totalAmount": 299.00
  }
}

// 不好的事件命名(命令式)
{
  "eventType": "ProcessOrder",  // 这是命令,不是事件
  ...
}

我在项目中遇到过有人把事件命名成“UpdateOrderStatus”,这其实是命令。事件应该是“OrderStatusUpdated”。记住:事件是过去时,命令是祈使句

2. 事件处理器(Event Handler)

处理器就是“听到事件后干活的人”。一个事件可以被多个处理器消费,也可以被零个处理器消费——这完全取决于业务需求。

// 一个事件,多个处理器
public class OrderPlacedHandler {
  
  @EventListener
  public void onOrderPlaced(OrderPlacedEvent event) {
    // 处理器1:发送确认邮件
    emailService.sendConfirmation(event.getOrderId());
  }
  
  @EventListener
  public void updateInventory(OrderPlacedEvent event) {
    // 处理器2:更新库存
    inventoryService.deductStock(event.getOrderId());
  }
  
  @EventListener
  public void notifyLogistics(OrderPlacedEvent event) {
    // 处理器3:通知物流
    logisticsService.prepareShipment(event.getOrderId());
  }
}

避坑指南:我曾经在一个项目中,让一个处理器干了三件事——发邮件、更新库存、通知物流。结果邮件服务挂了,整个处理都回滚了。后来拆成三个独立处理器,各管各的,互不影响。记住:处理器要单一职责

3. 事件通道(Event Channel)

通道是事件从生产者到消费者的“高速公路”。常见的实现有:

  • 消息队列:RabbitMQ、Kafka、AWS SQS
  • 事件总线:Guava EventBus、Spring ApplicationEvent
  • 流处理平台:Apache Kafka Streams、Flink

我个人习惯用 Kafka 做跨服务的事件通道,用 Spring 的 ApplicationEvent 做单体应用内部的事件。选型时主要看两点:是否需要持久化是否需要回溯消费

三种核心模式

事件驱动架构不是一种模式,而是三种模式的集合。我画了一张图帮你理清关系:

事件驱动架构三种模式 事件驱动架构 事件通知 生产者→通道→消费者 事件溯源 事件存储 + 状态重建 CQRS 命令模型 + 查询模型 事件通知 最简单的模式。组件通过事件通道广播消息,消费者按需处理。 典型场景:订单创建后通知库存、物流、财务等多个系统。 事件溯源 不存当前状态,只存事件序列。需要状态时,从头回放所有事件。 典型场景:审计系统、银行交易流水、版本控制。 CQRS 命令和查询分离。写操作走命令模型,读操作走查询模型。 典型场景:读写负载差异大的系统,如电商、社交平台。 三者关系 事件通知是基础,事件溯源是存储策略,CQRS是读写分离。 它们可以独立使用,也可以组合使用(如事件溯源 + CQRS)。

模式一:事件通知(Event Notification)

这是最基础的模式。生产者发出事件,消费者收到后执行自己的逻辑。双方完全不知道对方的存在。

举个例子:用户下单后,系统发出 OrderPlaced 事件。邮件服务收到后发确认邮件,库存服务收到后扣库存,物流服务收到后安排配送。每个服务各干各的,互不干扰。

适用场景:微服务之间的异步通信、系统解耦、多系统协作。

模式二:事件溯源(Event Sourcing)

这个模式很有意思。传统做法是存“当前状态”,比如订单表里存“已支付”。事件溯源不这么干——它只存“发生了什么”。

比如一个订单的生命周期,存的是:

  1. OrderCreated(订单创建)
  2. OrderPaid(订单支付)
  3. OrderShipped(订单发货)
  4. OrderDelivered(订单送达)

想知道当前状态?把上面的事件从头到尾回放一遍就知道了。

// 事件溯源示例:从事件流重建订单状态
public class Order {
  private String orderId;
  private OrderStatus status;
  private List<Event> events = new ArrayList<>();
  
  public Order(List<Event> eventStream) {
    for (Event event : eventStream) {
      apply(event);
    }
  }
  
  private void apply(Event event) {
    this.events.add(event);
    if (event instanceof OrderCreated) {
      this.status = OrderStatus.CREATED;
    } else if (event instanceof OrderPaid) {
      this.status = OrderStatus.PAID;
    } else if (event instanceof OrderShipped) {
      this.status = OrderStatus.SHIPPED;
    }
    // 以此类推...
  }
}

注意:事件溯源不是银弹。我曾在项目中过度使用它,结果发现查询当前状态需要回放几千个事件,性能惨不忍睹。解决方案是定期做“快照”(Snapshot),把某个时间点的状态存下来,之后的事件从快照开始回放。

模式三:CQRS(命令查询职责分离)

CQRS 的核心思想是:写数据和读数据用不同的模型

你想想看,一个电商系统的商品详情页,读操作可能涉及几十个字段的聚合,而写操作可能只更新价格和库存。如果读写用同一个模型,要么读模型臃肿,要么写模型受限。

CQRS 的做法是:

  • 命令模型(Command Model):处理写操作,关注数据一致性
  • 查询模型(Query Model):处理读操作,关注查询性能和展示
// 命令模型:处理写操作
public class OrderCommandService {
  public void placeOrder(PlaceOrderCommand command) {
    // 验证、校验、持久化
    Order order = new Order(command.getCustomerId(), command.getItems());
    orderRepository.save(order);
    eventPublisher.publish(new OrderPlacedEvent(order.getId()));
  }
}

// 查询模型:处理读操作
public class OrderQueryService {
  public OrderSummary getOrderSummary(String orderId) {
    // 直接从专门的读数据库查询,不需要关联查询
    return orderSummaryRepository.findById(orderId);
  }
}

我的经验:CQRS 和事件溯源是天生一对。事件溯源提供事件流,CQRS 用这些事件构建专门的读模型。我在一个金融项目中就是这么干的——写模型保证事务一致性,读模型提供毫秒级查询,爽得很。

优缺点分析

维度 优点 缺点
耦合度 组件之间松耦合,修改一个不影响其他 事件结构变更时,所有消费者都需要适配
扩展性 可以轻松增加新的消费者,不影响现有系统 事件流难以调试,问题追踪困难
性能 异步处理,不阻塞主流程 增加了网络开销和消息序列化/反序列化成本
可靠性 事件持久化后可以重试,不会丢失 最终一致性导致数据短暂不一致
可追溯性 事件溯源模式下,所有变更都有完整记录 事件数量巨大时,存储和查询成本高

应用场景

说了这么多,到底什么时候该用事件驱动?我总结了几个典型场景:

  1. 跨系统异步协作:订单系统、支付系统、物流系统之间的消息传递
  2. 实时数据处理:物联网设备数据采集、用户行为分析、监控告警
  3. 审计与合规:所有操作都需要记录不可篡改的审计日志
  4. 读写分离需求:读操作远多于写操作,且查询维度复杂
  5. 系统解耦重构:把单体应用拆分成微服务时,事件驱动是很好的过渡方案

什么时候别用?如果业务逻辑是强事务性的(比如银行转账,必须保证原子性),或者系统规模很小(就两三个服务),事件驱动反而会增加复杂度。别为了用而用。

小结

事件驱动架构的核心价值就四个字:解耦、异步。它让系统变得更灵活、更可扩展,但也带来了最终一致性和调试复杂度的问题。

我个人建议:从事件通知模式开始,等真正遇到性能瓶颈或审计需求时,再考虑引入事件溯源和 CQRS。别一上来就搞全套,容易把自己绕进去。

嗯,今天就聊到这儿。记住:架构没有银弹,只有适合不适合。


公众号:蓝海资料掘金营,微信deep3321