@TransactionalEventListener 是 Spring 框架提供的一个注解,用于在事务提交后或其他事务状态下执行事件监听器的方法。大多数情况下(也是默认的情况),我们一般会在事务提交后执行一些操作,例如发送消息、记录日志等。

@TransactionalEventListener 会在事务成功提交之后才触发监听器方法。如果我们对其运行机制理解不够深入,在使用过程中,很容易踩到坑。

问题说明

假设我们有这样一个业务场景:用户发表文章后需要记录用户操作,并在用户操作记录后发送通知。具体流程如下:

  1. 发表文章,发布"文章已发布"事件
  2. 监听文章发布事件,记录用户操作
  3. 操作记录保存后,发布"操作已记录"事件
  4. 处理操作记录后的通知发送逻辑

看起来很简单的流程,但实际实现时却踩到了坑。让我们看看具体的代码实现:

@Service
public class ArticleService {
    @Transactional
    public void publishArticle(Article article) {
        String articleId = "A" + System.currentTimeMillis();
        article.setId(articleId);
        articleRepository.save(article);
        applicationContext.publishEvent(new ArticlePublishedEvent(articleId));
    }
}

@Component
public class ArticlePublishedEventListener {
    @TransactionalEventListener
    public void handle(ArticlePublishedEvent event) {
        userActionService.recordAction(event.getUserId(), "发表文章", event.getArticleId());
    }
}

@Service
public class UserActionService {
    @Transactional
    public void recordAction(Long userId, String action, String targetId) {
        UserAction userAction = new UserAction(userId, action, targetId);
        userActionRepository.save(userAction);
        applicationContext.publishEvent(new ActionRecordedEvent(userId, action));
    }
}

@Component
public class ActionRecordedEventListener {

    @TransactionalEventListener
    public void handle(ActionRecordedEvent event) {
        // 发送用户操作通知
        log.info("用户 {} 的操作已记录: {}", event.getUserId(), event.getAction());
    }

}

运行测试后发现:

  • 文章数据成功保存
  • 用户操作记录没有写入数据库!
  • “操作已记录” 事件的监听器没有被触发!

问题分析

通过查看 @TransactionalEventListener 的文档注解,我们可以发现有一则 WARNING:

WARNING: if the TransactionPhase is set to AFTER_COMMIT (the default), AFTER_ROLLBACK, or AFTER_COMPLETION, the transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still “participate” in the original transaction, but changes will not be committed to the transactional resource. See TransactionSynchronization. afterCompletion(int) for details.

这段话的关键点是:

  1. 在默认的 AFTER_COMMIT 阶段,原事务已经提交
  2. 事务资源(如数据库连接)可能仍然存在
  3. 此时的数据访问操作虽然能执行,但不会被提交

让我们分析一下前面的场景:

  1. ArticleService 发布了 ArticlePublishedEvent 事件。
  2. ArticlePublishedEventListener 监听到事件后,调用 UserActionService.recordAction() 记录用户操作。
  3. UserActionService.recordAction() 又发布了 ActionRecordedEvent 事件。
  4. ActionRecordedEventListener 监听 ActionRecordedEvent 事件并记录日志。

问题出在:

  1. ArticlePublishedEventListener 使用了默认的 AFTER_COMMIT 事务阶段。这意味着它会在 ArticleService 的事务提交后才执行。
  2. UserActionService.recordAction() 尝试保存用户操作时,它实际上仍在使用已提交的事务资源。根据警告说明,这种情况下的数据库操作不会被提交。
  3. 同理,ActionRecordedEvent 的监听器也因为同样的原因没有被触发,因为参与的事务已提交,不存在提交事务的情况了。

这就解释了为什么:

  • 文章能保存成功(在原始事务中完成)
  • 用户操作记录没有写入(在已提交的事务资源中操作)
  • “操作已记录” 事件的监听器没有触发(事件在无效的事务上下文中发布)

解决方案

使用新事务

最简单的解决方案是让 ArticlePublishedEventListener 在新事务中执行。只需要在监听器方法上添加 @Transactional 注解,并设置 propagationPropagation.REQUIRES_NEW

@Component
public class ArticlePublishedEventListener {

    @TransactionalEventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handle(ArticlePublishedEvent event) {
        userActionService.recordAction(event.getUserId(), "发表文章", event.getArticleId());
    }

}

这样,数据更改操作就会在开启的新事务中被正常提交。

但是,如果 UserActionService.recordAction() 方法执行时间较长,会导致 ArticleService 的所占用的连接长时间不释放,若此时数据库连接池配置了最大连接数,可能会导致连接池耗尽,进而导致系统不可用。

使用 @Async 声明异步执行

结合 @Async 使用,可以确保事件监听器在单独的线程中执行,从而使用独立的事务上下文。

@Component
public class ArticlePublishedEventListener {

    @Async
    @TransactionalEventListener
    public void handle(ArticlePublishedEvent event) {
        userActionService.recordAction(event.getUserId(), "发表文章", event.getArticleId());
    }

}

适用场景

基于上面的问题,就可以分析总结出一些使用场景:

非事务性的后续处理

@Component
public class ArticleEventListener {

    @TransactionalEventListener
    public void handleArticlePublished(ArticlePublishedEvent event) {
        // 发送邮件通知
        emailService.sendNotification(event.getUserId());
        // 更新搜索索引
        searchIndexService.updateIndex(event.getArticleId());
        // 发送消息到消息队列
        messagingService.sendToQueue(event);
    }

}

这些操作的特点是:

  • 不需要事务支持
  • 失败后可以重试
  • 与主流程解耦

只读操作

@Component
public class UserStatisticsListener {
    @TransactionalEventListener
    public void updateStatistics(UserActionEvent event) {
        // 读取统计数据
        Statistics stats = statisticsRepository.findByUserId(event.getUserId());

        // 发送到监控系统
        monitoringService.report(stats);
    }
}

适合进行:

  • 数据查询
  • 统计分析
  • 监控报告

异步处理任务

@Component
public class AsyncTaskListener {
    @TransactionalEventListener
    @Async
    public void handleAsyncTask(BusinessEvent event) {
        // 执行耗时操作
        // 注意:这里的操作应该是幂等的
        timeConsumingService.process(event);
    }
}

特别适合:

  • 耗时操作
  • 需要解耦的任务
  • 不影响主流程的后台任务