news 2026/6/17 20:39:00

Spring 的事件机制你用了三年,但 @TransactionalEventListener 的坑一个都没绕过去

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 发布消息,动物们各自订阅自己关心的内容。感兴趣的可以搜一下,或者等我后面的文章,每个模式我都会同步对应的小程序内容。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/17 20:36:04

CodeWarrior IDE调试实战:从断点、事件点到多核与外部构建集成

1. 调试器核心概念与工作流解析调试,对于每一位开发者而言,都是将抽象逻辑与现实问题连接起来的桥梁。它远不止是“找Bug”,而是一个系统性的观察、分析和干预程序运行状态的过程。在嵌入式开发、系统软件乃至复杂的桌面应用开发中&#xff0…

作者头像 李华
网站建设 2026/6/17 20:30:41

终极容器镜像加速指南:5分钟解决国外镜像拉取超时难题

终极容器镜像加速指南:5分钟解决国外镜像拉取超时难题 【免费下载链接】public-image-mirror 很多镜像都在国外。比如 gcr 。国内下载很慢,需要加速。致力于提供连接全世界的稳定可靠安全的容器镜像服务。 项目地址: https://gitcode.com/GitHub_Trend…

作者头像 李华
网站建设 2026/6/17 20:23:21

GPT-5.5时代岗位能力压力测试实操指南

1. 这不是新闻通稿,而是一次岗位能力压力测试的实操记录“GPT-5.5来了,你的岗位还有多少天?”——这句话最近在几个行业群和内部复盘会上被反复拎出来,不是当段子讲,而是真有人拿着它去对照自己的周报、项目SOP、甚至上…

作者头像 李华
网站建设 2026/6/17 20:23:05

计算机毕业设计之同城搬家服务平台设计与实现

随着城市化进程的加快,人口流动日益频繁,同城搬家需求不断增长。为满足这一需求,同城搬家服务平台应运而生,它采用了Node.js语言、Express框架和MySQL数据库技术,构建了一个高效、便捷、可靠的在线搬家服务平台。在系统…

作者头像 李华
网站建设 2026/6/17 20:17:56

换管理系统前,美容院必须和供应商确认的5个数据问题

美容院更换管理系统不会导致会员数据丢失,但如果缺乏规划,储值余额、次卡次数和消费记录确实存在出错风险。按照成熟的迁移方法执行,会员主数据(姓名、手机号、余额、次卡等)的完整迁移成功率可达到95%–99%&#xff0…

作者头像 李华