一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

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

时间:2026-06-18 09:12:08 编辑:袖梨 来源:一聚教程网

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

事情是这样的:用户下单后要发短信通知。我在 OrderService 里写了个 @EventListener

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

热门栏目