Spring @EventListener 事件驱动解耦实战:同步转异步、事务绑定与顺序控制

Spring @EventListener 事件驱动解耦实战:同步转异步、事务绑定与顺序控制

一个「用户注册」的 service 方法,时间长了往往会变成一坨:存库、发欢迎邮件、送积分、写审计日志、推送到风控......业务主流程被一堆「注册成功后顺便要做的事」塞满,加一个下游就得改一次注册代码,还得改它的单测。

Spring 的事件机制(ApplicationEventPublisher + @EventListener)就是拆这坨的利器:主流程只管「发一个事件」,谁关心谁自己订阅。这篇把发布订阅、异步、事务绑定、顺序这几个实战要点一次讲清,并点出最容易踩的「事务里发邮件」坑。

从耦合的写法开始

注册方法一开始长这样,什么都往里塞:

java 复制代码
@Service
public class UserService {
    // 一堆下游依赖全怼进来,注册和它们死死耦合
    private final UserRepository userRepo;
    private final EmailService emailService;
    private final PointService pointService;
    private final AuditService auditService;

    public void register(RegisterCmd cmd) {
        User user = userRepo.save(new User(cmd.username(), cmd.email()));
        emailService.sendWelcome(user);   // 加一个下游就改这里
        pointService.grantSignupBonus(user);
        auditService.log("register", user.getId());
    }
}

问题很直接:UserService 依赖了四个东西,新增「注册后发短信」又要改它、改它的测试;而且发邮件慢,把注册接口拖慢了。

第一步:定义事件并发布

先把「注册成功」抽象成一个事件对象(用 record 最干净):

java 复制代码
// 一个不可变的事件载体
public record UserRegisteredEvent(Long userId, String username, String email) {}

主流程只注入 ApplicationEventPublisher,存完库发个事件就收工:

java 复制代码
@Service
public class UserService {
    private final UserRepository userRepo;
    private final ApplicationEventPublisher publisher; // Spring 自动注入

    public UserService(UserRepository userRepo, ApplicationEventPublisher publisher) {
        this.userRepo = userRepo;
        this.publisher = publisher;
    }

    @Transactional
    public void register(RegisterCmd cmd) {
        User user = userRepo.save(new User(cmd.username(), cmd.email()));
        // 只管发事件,不关心谁在听
        publisher.publishEvent(
            new UserRegisteredEvent(user.getId(), user.getUsername(), user.getEmail()));
    }
}

UserService 现在只依赖仓库和发布器,下游全砍掉了。

第二步:用 @EventListener 订阅

每个下游各写各的监听器,互不打扰:

java 复制代码
@Component
public class WelcomeEmailListener {
    private final EmailService emailService;

    public WelcomeEmailListener(EmailService emailService) {
        this.emailService = emailService;
    }

    // 方法参数类型就是订阅的事件类型
    @EventListener
    public void on(UserRegisteredEvent event) {
        emailService.sendWelcome(event.email());
    }
}

@Component
public class SignupBonusListener {
    private final PointService pointService;

    public SignupBonusListener(PointService pointService) {
        this.pointService = pointService;
    }

    @EventListener
    public void on(UserRegisteredEvent event) {
        pointService.grantSignupBonus(event.userId());
    }
}

想加「注册后发短信」?新写一个 SmsListener 就行,碰都不用碰 UserService。这就是事件驱动解耦的价值。

关键坑:默认是同步的,而且在同一个事务里

很多人以为 publishEvent 是异步的, 。默认 @EventListener同步执行 的:publishEvent 会阻塞,直到所有监听器跑完才返回。这意味着:

  1. 发邮件慢,注册接口照样被拖慢(没解决性能问题)。
  2. 更危险的是------监听器和发布方跑在同一个事务里 。如果 WelcomeEmailListener 抛异常,默认会把 register 的事务一起回滚,用户根本没注册成功。「发邮件失败导致注册失败」,这是线上事故重灾区。

第三步:@TransactionalEventListener 绑定事务提交

正确姿势是让下游在事务真正提交之后 才执行,用 @TransactionalEventListener:

java 复制代码
@Component
public class WelcomeEmailListener {
    private final EmailService emailService;

    public WelcomeEmailListener(EmailService emailService) {
        this.emailService = emailService;
    }

    // 只在事务成功提交后才触发;事务回滚则不发邮件
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void on(UserRegisteredEvent event) {
        emailService.sendWelcome(event.email());
    }
}

phase 可选 AFTER_COMMIT(默认,提交后)、AFTER_ROLLBACKAFTER_COMPLETIONBEFORE_COMMIT。用 AFTER_COMMIT 就保证了「数据真落库了才发邮件」,不会出现「邮件发了但事务回滚、用户其实没注册」的鬼故事。

注意一个陷阱:@TransactionalEventListener 只有在存在活动事务时才会触发 。如果发事件的方法没有 @Transactional,这个监听器会被静默跳过。想让「没事务时也执行」,加 fallbackExecution = true

第四步:异步执行,别拖慢主流程

绑定了 AFTER_COMMIT 只是解决了正确性,发邮件仍是同步的、还在拖时间。加 @Async 让它扔到线程池后台跑:

java 复制代码
@Configuration
@EnableAsync   // 别忘了开启异步,否则 @Async 是哑的
public class AsyncConfig {
    @Bean
    public Executor eventExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("event-");
        executor.initialize();
        return executor;
    }
}
java 复制代码
@Component
public class WelcomeEmailListener {
    // AFTER_COMMIT 保证数据落库,@Async 让它后台异步跑,不阻塞注册接口
    @Async("eventExecutor")
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void on(UserRegisteredEvent event) {
        emailService.sendWelcome(event.email());
    }
}

两个提醒:一是 @Async 一旦异步,监听器里的异常不会再传播给主流程 ,得自己在监听器里 try-catch 记日志,否则异常被线程池悄悄吞掉;二是别用默认线程池(SimpleAsyncExecutor 每次新建线程,不复用),一定自定义一个有界队列的池,防止任务堆积打爆内存。

控制多个监听器的执行顺序

同一个事件有多个监听器时,默认顺序不确定。要指定顺序用 @Order,数字越小越先执行:

java 复制代码
@Component
public class AuditListener {
    @Order(1)  // 先记审计
    @EventListener
    public void on(UserRegisteredEvent event) { /* ... */ }
}

@Component
public class SignupBonusListener {
    @Order(2)  // 再送积分
    @EventListener
    public void on(UserRegisteredEvent event) { /* ... */ }
}

但要清醒:一旦监听器加了 @Async 各自跑在不同线程,@Order不保证真实完成顺序了,它只保证提交给线程池的先后。有强顺序依赖的逻辑,别拆成两个异步监听器。

小结

  • ApplicationEventPublisher.publishEvent + @EventListener 把「主流程」和「下游副作用」解耦,加下游不再改主流程代码。
  • 默认同步、且在同一事务里:监听器抛异常会回滚主流程,「发邮件失败导致注册失败」就是这么来的。
  • 正确性用 @TransactionalEventListener(AFTER_COMMIT) 保证「数据落库后才执行副作用」;注意没事务时它默认不触发,需要 fallbackExecution=true
  • 性能用 @Async + @EnableAsync + 自定义有界线程池,并记得异步后异常要自己 catch,别让线程池吞掉。
  • 顺序用 @Order;但异步之后 @Order 只管提交顺序不管完成顺序。

一句话记忆:发事件解耦,AFTER_COMMIT 保正确,@Async 保性能------三者缺一,不是拖慢主流程就是把主流程拖下水。

相关推荐
del1 小时前
第十周技术博客
数据库
一木 之林1 小时前
插件、MCP、Skill 的区别?
java·c++·人工智能
1314lay_10071 小时前
C#调用Sql Server存储过程,并且使用传入表结构
数据库·经验分享·笔记·sqlserver·c#
小刘在重生~1 小时前
面试重点:final、finalize、finally 三者区别
java
anxiao_m1 小时前
2026大文件传输软件推荐,从速度安全多维度客观测评
数据库·文件传输·传输
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day39|SpringAI 会话存储、Function‑Call 工具调用、MCP 思维导图复盘
java·开发语言·spring boot·学习·阿里云
IT大白鼠1 小时前
MySQL 分布式集群系列 · 第七篇——生产调优实战:NDB 性能、内存、高可用全方位优化
数据库·分布式·mysql
ellenwan20261 小时前
看到“最新 AI 量化学习”时,先让表达变清楚
人工智能·python
IvorySQL2 小时前
打造下一代 AI Agent 的统一多模智能数据底座——PostgreSQL 与 AI 的融合演进
数据库·人工智能·postgresql