最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
Spring 的事件机制你用了三年:但 @TransactionalEventListener 的坑一个都没绕过去
时间:2026-06-18 09:12:08 编辑:袖梨 来源:一聚教程网
Spring 的事件机制你用了三年,但 @TransactionalEventListener 的坑一个都没绕过去
事情是这样的:用户下单后要发短信通知。我在 OrderService 里写了个 @EventListener:

@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 就是为这个场景设计的:
@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
// 事务提交成功 → 执行
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)// 事务结束(无论提交还是回滚)→ 都会执行
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
大多数业务通知应该用 AFTER_COMMIT——只有真正写库成功了才发。但如果你要在回滚时发一个"下单失败"的消息,就得用 AFTER_COMPLETION 配合判断事务状态。
另外,AFTER_COMMIT 的 Listener 如果自己抛了异常,不会回滚主事务——因为主事务已经提交了。也就是说,发短信失败不会让订单回滚。这是你想要的吗?不一定。如果你的业务要求"短信发不出去订单就不能算完成",那 AFTER_COMMIT 就不适合——你得回到事务内同步调用。
坑二:@Async 的线程池不隔离,慢任务拖死整个系统
Spring 默认的 @Async 线程池是 SimpleAsyncTaskExecutor——这玩意每次创建一个新线程,没有上限。高并发下直接 OOM。
你必须自己配置线程池:
@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
这应该是踩坑率最高的问题了:
// 异步事件监听器
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
// 异步线程里这玩意儿是 null
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
因为 @Async 在新线程执行,RequestContextHolder 是基于 ThreadLocal 的,新线程里没有。解决方案:在事件里携带需要的上下文,而不是在 Listener 里现取。
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 注解:
@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 发布消息,动物们各自订阅自己关心的内容。感兴趣的可以搜一下,或者等我后面的文章,每个模式我都会同步对应的小程序内容。
相关文章
- 通义千问企业版应用场景:办公协同与文档处理边界 06-18
- 原神如何获取最后三个星星果 06-18
- 智谱清言企业版写作使用:权限范围与输出限制说明 06-18
- Kimi企业版权限分配与团队协作使用要点 06-18
- 胜利女神新的希望拦截战怎么打 06-18
- 2026年通义千问提示词模板:适用场景与配置说明 06-18