1. 先把问题摊开:微服务为什么越拆越“拧巴”
做微服务的人几乎都会遇到同一个怪圈:服务拆得越细,系统反而越难维护。订单服务要调库存服务,库存服务要调商品服务,商品服务可能还要调优惠券服务,一个用户下单的请求,拉出一条长到看不见头的同步调用链。任何一个下游服务稍微慢一点,整个链路跟着抖,接口超时、线程阻塞、数据库连接池被打满,线上告警一轮接一轮。
我见过不少团队在单体时代还挺稳的系统,拆成微服务之后反而天天出事故。原因不是微服务本身有问题,而是服务之间的通信方式没有被认真设计。默认一看是微服务,就直接上Feign、OpenFeign、RestTemplate,服务跟服务之间全部走同步HTTP调用,代码写起来确实直观,但耦合一点没少,只是从“代码级别耦coupled”变成了“网络级别耦合”。
这时候就需要换一种思路:能不能让服务之间不直接“喊话”,而是通过“广播”来通信? 这正是事件驱动的核心价值。服务A不关心服务B是否存在,只需要把“发生了什么”发布出去,谁感兴趣谁自己来接。A不用等B的响应,B也不影响A的可用性。这种模式下的服务,彼此的依赖被降到最低,真正实现松耦合。
这篇文章我会带你完整走一遍在Spring生态下构建事件驱动架构的路径,从最基础的ApplicationEvent到Spring Cloud Stream整合消息队列,结合实际代码和踩坑经历,把“事件驱动”从概念落地成能上生产环境的东西。适合正在做微服务改造,或者被服务间同步调用折磨得不行的后端开发,也适合想系统理解Spring事件机制的进阶读者。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 事件驱动到底在解什么题
2.1 从一次“简单”的订单创建说起
先看一个最常见不过的业务场景:用户下单。单体时代这个流程简单直接,一个事务里把订单表插进去、扣库存、加积分、发通知,全部在一个方法里搞定,出了问题要么整体回滚,要么事后补偿。
到了微服务阶段,订单、库存、积分、通知各是一个独立服务。用同步调用实现的话,代码大概长这样:
java复制@Transactional
public void createOrder(OrderDTO order) {
// 1. 保存订单
orderRepository.save(order);
// 2. 调用库存服务扣减库存
stockClient.deduct(order.getSkuId(), order.getCount());
// 3. 调用积分服务增加积分
pointClient.addPoints(order.getUserId(), order.getAmount());
// 4. 调用通知服务发送消息
notifyClient.send(order.getUserId(), "下单成功");
}
第一眼看过去没什么问题,但你把这次请求放进真实环境里想一想:
- 下单高峰期,积分服务一个慢查询把响应时间拉到3秒,所有下单请求全部卡在这条链路上,用户端直接超时。
- 通知服务因为网络抖动挂了,订单服务也跟着报错,本该成功的订单也没了。
- 后面再要增加一个“下单后赠送优惠券”的需求,你需要在订单服务里再加一个
couponClient的调用,改代码、发版本、重新部署。
这三个问题,本质上都是同步调用带来的副作用:强依赖、共享可用性、生产成本耦合。订单服务只是一个发号施令的人,却要为所有下游服务的可用性和性能买单。
2.2 事件驱动的核心模型:事件、生产者、消费者、事件中心
事件驱动的做法完全不同。订单服务只管一件事:把“订单已创建”这个事实发布出去,后面的事情一概不关心。
这个模式里有四个核心角色:
- 事件(Event):描述“已经发生的事情”,比如
OrderCreatedEvent,里面携带业务数据(订单ID、用户ID、金额)。 - 生产者(Producer):发布事件的一方。订单服务就是生产者,它在订单数据落库后发布
OrderCreatedEvent。 - 消费者(Consumer):订阅并处理事件的一方。积分服务、通知服务都是消费者,它们收到
OrderCreatedEvent后各干各的活。 - 事件中心(Event Broker):负责事件的传递和路由。它可以简单到Spring容器本身,也可以是一个独立的消息中间件,比如RabbitMQ、Kafka。
用事件驱动重写上面的下单流程:
java复制@Transactional
public void createOrder(OrderDTO order) {
orderRepository.save(order);
// 发布事件,后面的事情交给“感兴趣的人”
eventPublisher.publish(new OrderCreatedEvent(order.getId(), order.getUserId(), order.getAmount()));
}
事件发布完之后,这个方法就结束返回了。积分加不加、通知发不发、优惠券送不送,全是其他服务自己的事。订单服务的接口响应时间只取决于自身数据库写入速度,不再被下游拖累。
用一个直白的类比来理解:同步调用像打电话,你必须等对方接通、说完、挂了电话才能做下一件事,对方一直不接,你就得一直等。事件驱动像发朋友圈,你发一条状态,朋友看到后想点赞点赞、想评论评论,你发完就可以去忙自己的事,不用等任何人回应。朋友圈系统本身(事件中心)保证你的状态能送到该送的人那里。
2.3 为什么说松耦合是“顺手”得到的
很多人以为松耦合是一个需要刻意设计的目标,实际上在事件驱动架构里,它是自然产物。
生产者不依赖消费者的具体实现。订单服务发布OrderCreatedEvent时,它不认识积分服务,也不认识通知服务,它只认识这个事件。事件是双方的唯一契约,而不是某个RPC接口。
消费者的变化不影响生产者。积分服务想把“下单加10积分”改成“下单加10积分再送一张满减券”,这是积分服务自己的事情,订单服务不需要任何修改。新增一个数据分析服务来订阅OrderCreatedEvent做订单统计,订单服务也无感知。
可用性边界被隔离。通知服务挂掉,不影响订单创建这个核心链路。事件会暂存在事件中心,等通知服务恢复后继续消费,而不是跟着通知服务一起“陪葬”。
这就是事件驱动最迷人的地方:它不是靠约束和规范来保证服务不互相耦合,而是靠通信方式的改变让耦合根本没有发生的土壤。
3. 从Spring容器内的第一层解耦开始
3.1 ApplicationEvent:Spring内置的“小型事件中心”
如果你还没有引入任何消息中间件,只是想解决服务内部模块之间的耦合问题,Spring自带的ApplicationEvent机制就够用了。
ApplicationEvent是Spring对观察者模式的原生实现。核心思路是:某个Bean通过ApplicationEventPublisher发布事件,其他Bean通过@EventListener注解监听事件,发布者和监听者之间完全解耦。
定义一个事件。继承ApplicationEvent是传统的做法,但Spring 4.2之后事件类已经不需要强制继承它,任何普通POJO都可以作为事件,发布和监听的时候直接用泛型即可,代码更清爽:
java复制public class OrderCreatedEvent {
private final Long orderId;
private final Long userId;
private final BigDecimal amount;
public OrderCreatedEvent(Long orderId, Long userId, BigDecimal amount) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
}
// getter方法省略
}
在业务代码中注入发布器并发布事件:
java复制@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public OrderService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Transactional
public void createOrder(OrderDTO order) {
// 业务逻辑:保存订单
OrderEntity entity = orderRepository.save(convert(order));
// 发布事件
eventPublisher.publishEvent(new OrderCreatedEvent(entity.getId(), entity.getUserId(), entity.getAmount()));
}
}
在消费者中监听事件:
java复制@Component
public class PointListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 加积分逻辑
pointService.addPoints(event.
