Spring 的事件机制你用了三年,但 @TransactionalEventListener 的坑一个都没绕过去
Spring 的事件机制你用了三年,但 @TransactionalEventListener 的坑一个都没绕过去
事情是这样的:用户下单后要发短信通知。我在 OrderService 里写了个@EventListener:
```java @Service public class OrderService {
@Autowired private ApplicationEventPublisher publisher; @Transactional public void createOrder(OrderDTO dto) { Order order = orderRepo.save(dto.toEntity()); publisher.publishEvent(new OrderCreatedEvent(order)); // 发短信 smsService.send(order.getUserPhone(), "下单成功"); }} ```
测试环境一切正常。上线当天,客服电话被打爆——"我下单成功了但没收到短信"。
查日志发现,publishEvent是同步执行的,在事务提交前就发了短信。如果事务回滚了(比如库存扣减失败),短信已经发出去了——用户收到了"下单成功"但订单根本没创建。
我去掉publishEvent,直接在事务提交后调smsService。看起来解决了。然后产品说"下单成功还要发 App Push、记录用户行为、更新推荐算法"。我写了三个调用。又过一周,产品说"VIP 用户要多发一封邮件"。我又加一个。
到此为止,OrderService.createOrder() 里有 5 个非核心的副作用调用,每个都可能抛异常阻塞主流程。这就是所谓的观察者模式缺位导致的代码腐化。
观察者模式的正确姿势:不是你写个 Listener 就叫观察者了
很多 Java 工程师觉得"我用了 @EventListener 就是用了观察者模式",但实际情况是 90% 的人都在误用。
观察者模式的核心契约是这个:Subject 不应该知道 Observer 是谁,Observer 也不应该影响 Subject 的主流程。
对照这两个标准,上面那个例子两样都违反了: - OrderService 直接调用 smsService,知道 Observer 是谁 - Observer 的异常会阻断下单主流程
Spring 的@TransactionalEventListener就是为这个场景设计的:
```java @Service public class OrderService {
@Autowired private ApplicationEventPublisher publisher; @Transactional public void createOrder(OrderDTO dto) { Order order = orderRepo.save(dto.toEntity()); publisher.publishEvent(new OrderCreatedEvent(order)); // 代码到此为止。其他副作用由 Listener 负责 }}
@Component public class OrderNotificationListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { // 事务提交后才执行,不会在回滚时误发 smsService.send(event.getOrder().getUserPhone(), "下单成功"); }}
@Component public class OrderAnalyticsListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @Async // 异步执行,不阻塞主流程 public void onOrderCreated(OrderCreatedEvent event) { analyticsService.record("order_created", event.getOrder().getId()); }} ```
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)保证了 Listener 只在事务成功提交后执行。如果事务回滚,Listener 根本不会被触发。
加上@Async,分析埋点这类非关键操作就不会拖慢下单接口的响应时间。
你以为这就完了?生产环境会教做人的
坑一:AFTER_COMMIT 不是 AFTER_COMPLETION
```java // 事务提交成功 → 执行 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
// 事务结束(无论提交还是回滚)→ 都会执行 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION) ```
大多数业务通知应该用 AFTER_COMMIT——只有真正写库成功了才发。但如果你要在回滚时发一个"下单失败"的消息,就得用 AFTER_COMPLETION 配合判断事务状态。
另外,AFTER_COMMIT 的 Listener 如果自己抛了异常,不会回滚主事务——因为主事务已经提交了。也就是说,发短信失败不会让订单回滚。这是你想要的吗?不一定。如果你的业务要求"短信发不出去订单就不能算完成",那 AFTER_COMMIT 就不适合——你得回到事务内同步调用。
坑二:@Async 的线程池不隔离,慢任务拖死整个系统
Spring 默认的@Async线程池是SimpleAsyncTaskExecutor——这玩意每次创建一个新线程,没有上限。高并发下直接 OOM。
你必须自己配置线程池:
```java @Configuration @EnableAsync public class AsyncConfig {
@Bean("eventExecutor") public Executor eventExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy() // 满了让调用线程执行 ); executor.setThreadNamePrefix("event-"); executor.initialize(); return executor; }}
// Listener 指定线程池 @Async("eventExecutor") @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { // ... } ```
这里还有一个容易被忽略的细节:CallerRunsPolicy——当队列满了,任务由发布事件的线程(也就是你的 HTTP 线程)自己执行。这听起来会让接口变慢,但比直接丢弃任务导致数据丢失要好。取舍在于你的业务对延迟的容忍度。
坑三:异步事件里拿不到 HttpServletRequest
这应该是踩坑率最高的问题了:
java // 异步事件监听器 @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { // ❌ 异步线程里这玩意儿是 null HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); }
因为@Async在新线程执行,RequestContextHolder 是基于 ThreadLocal 的,新线程里没有。解决方案:在事件里携带需要的上下文,而不是在 Listener 里现取。
```java public class OrderCreatedEvent { private final Order order; private final String clientIp; // 从 request 里取出放到事件里 private final String userAgent;
public OrderCreatedEvent(Order order, HttpServletRequest request) { this.order = order; this.clientIp = request.getRemoteAddr(); this.userAgent = request.getHeader("User-Agent"); }} ```
事件的职责不只是"通知",还应该携带 Observer 需要的全部数据。
坑四:事件顺序没有保证
@EventListener和@TransactionalEventListener的多个 Listener 之间,执行顺序是不确定的。
如果 A Listener 必须比 B Listener 先执行(比如先更新缓存再发消息),你得用@Order注解:
```java @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @Order(1) public void updateCache(OrderCreatedEvent event) { ... }
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @Order(2) public void notifyMq(OrderCreatedEvent event) { ... } ```
但如果有多层依赖关系,@Order就会变得脆弱。更好的方案是:用消息队列替代 Spring Event 做跨服务的事件传递。Spring Event 适合进程内的轻量级解耦,跨服务的事件驱动还是交给 MQ 更靠谱。
什么时候不该用观察者模式
Spring Event 好用到容易滥用。有几个场景应该克制:
场景一:需要强一致性的操作。比如"创建订单 + 扣库存"——这不适合用事件解耦,因为扣库存失败必须回滚订单。这类操作应该在同一事务内完成。
场景二:事件数量爆炸。一个操作发布 20 个事件,每个事件有 3 个 Listener,你根本追踪不到整个调用链。与其用事件满天飞,不如回归到明确的流程编排(中介者模式/编排器)。
场景三:只有两个组件通信。如果 A 只需要通知 B,直接调用比绕一层事件更清晰。观察者模式的价值在 Observer 数量 ≥ 3 时才真正体现出来。
实际经验总结
Spring Event 机制是观察者模式的工程化实现,但它不是银弹。
正确用法:@TransactionalEventListener(AFTER_COMMIT)+ 自定义线程池 + 事件携带完整上下文 +@Order控制顺序。
但一旦你发现自己在写第 10 个@EventListener,就该停下来想一想了——你是不是在用事件机制逃避架构设计?把正常的流程编排拆成一堆离散的 Listener,除了让调用链不可追踪之外,没有任何好处。
观察者模式解决的是"Subject 不依赖 Observer"的问题,不是"我不知道自己代码在干嘛"的问题。
我正在做一个小程序叫「爪爪代码冒险记」,用卡皮巴拉的漫画故事讲 23 个设计模式,观察者模式这一集是森林广播站的故事——猫头鹰当 Subject 发布消息,动物们各自订阅自己关心的内容。感兴趣的可以搜一下,或者等我后面的文章,每个模式我都会同步对应的小程序内容。
