如何在 Spring 事务提交后执行回调

在业务开发中,我们经常会遇到这样一种场景:

某段逻辑必须等数据库事务成功提交之后才能执行。

例如:

  • 创建订单成功后发送消息;
  • 用户注册成功后发送欢迎邮件;
  • 数据落库成功后刷新缓存;
  • 业务状态变更成功后通知外部系统;
  • 事务提交后再发布领域事件。

这些逻辑看起来很简单,但如果执行时机不对,很容易引发数据不一致问题。

比如在一个事务方法中,我们先保存数据库,再发送 MQ 消息:

@Transactional
public void createOrder(CreateOrderCommand command) {
    orderRepository.save(order);

    mqProducer.send(orderCreatedMessage);
}

这段代码的问题在于:消息发送成功,并不代表数据库事务一定提交成功。

如果 mqProducer.send(...) 执行成功后,事务在提交阶段失败或被回滚,那么下游系统就会收到一条“订单已创建”的消息,但数据库里并没有这条订单记录。这就造成了典型的数据不一致。

因此,在很多场景下,我们真正想要的不是“业务代码执行到这里就发送消息”,而是:

只有当前事务成功提交之后,才执行后续逻辑。

一个简单的工具类

基于 Spring 的事务同步机制,我们可以封装一个工具方法:

import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public final class TransactionUtils {

    private TransactionUtils() {
    }

    /**
     * 在当前事务成功提交后执行指定逻辑;如果当前没有真实事务,则立即执行。
     *
     * @param runnable 事务提交后需要执行的逻辑
     */
    public static void runAfterCommit(Runnable runnable) {
        if (TransactionSynchronizationManager.isSynchronizationActive()
                && TransactionSynchronizationManager.isActualTransactionActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    runnable.run();
                }
            });
            return;
        }
        runnable.run();
    }
}

这个方法的语义非常明确:

  • 如果当前线程中存在一个真实事务,则把逻辑注册到事务提交后的回调中;
  • 如果当前没有真实事务,则直接执行这段逻辑。

使用方式如下:

@Transactional
public void createOrder(CreateOrderCommand command) {
    Order order = orderRepository.save(command.toOrder());

    TransactionUtils.runAfterCommit(() -> {
        mqProducer.send(new OrderCreatedMessage(order.getId()));
    });
}

这样一来,消息发送逻辑就不会在事务尚未提交时执行,而是会等到当前事务成功提交之后再执行。

为什么要判断两个状态?

代码中有两个判断:

TransactionSynchronizationManager.isSynchronizationActive()
        && TransactionSynchronizationManager.isActualTransactionActive()

这两个判断看起来有点相似,但含义并不完全一样。

1. isSynchronizationActive

isSynchronizationActive() 表示当前线程是否开启了事务同步机制。

事务同步机制可以理解为 Spring 提供的一套事务生命周期回调能力。只有同步机制处于激活状态时,才允许注册 TransactionSynchronization 回调。

如果没有激活事务同步,直接调用:

TransactionSynchronizationManager.registerSynchronization(...)

会抛出异常。

因此,第一个判断用于保证:

当前线程允许注册事务同步回调。

2. isActualTransactionActive

isActualTransactionActive() 表示当前线程是否真的存在一个实际事务。

这一步也很关键。

因为在某些情况下,事务同步可能是激活的,但并不代表当前一定存在真实的数据库事务。比如某些事务传播行为、只读上下文或框架内部同步场景,都可能出现“同步机制存在,但实际事务不存在”的情况。

而这个工具方法的语义是“事务提交后执行”。如果根本没有真实事务,就不存在所谓的“提交之后”。

所以第二个判断用于保证:

当前确实存在一个真实事务,afterCommit 回调才有意义。

两个条件同时成立时,才注册 afterCommit 回调。

为什么没有事务时要立即执行?

工具方法中还有一段兜底逻辑:

runnable.run();

也就是说,如果当前没有真实事务,就直接执行。

这是一种比较实用的设计。

原因是这个工具方法的调用方通常只关心:

这段逻辑不要早于事务提交执行。

如果当前本来就没有事务,那么就不存在“提交之后”的时机,也没有必要强行延迟执行。此时立即执行反而更符合直觉。

例如:

public void sendWelcomeMessage(Long userId) {
    TransactionUtils.runAfterCommit(() -> {
        messageService.sendWelcomeMessage(userId);
    });
}

这个方法既可以在事务方法中调用,也可以在非事务方法中调用。

如果在事务中调用,它会延迟到事务提交后执行。

如果不在事务中调用,它会立即执行。

这让业务代码不需要关心当前是否处于事务上下文中,调用方式更加统一。

afterCommit 的语义

这里使用的是:

@Override
public void afterCommit() {
    runnable.run();
}

afterCommit 的含义是:当前事务已经成功提交之后执行。

这和在业务方法末尾直接执行逻辑不同。

@Transactional 方法中,业务方法返回并不等于事务已经提交。Spring 的声明式事务通常是通过 AOP 实现的,事务提交发生在代理逻辑中,而不是业务方法内部的最后一行代码。

也就是说:

@Transactional
public void doSomething() {
    businessOperation();

    // 这里仍然处于事务方法内部
    doAfterSomething();
}

doAfterSomething() 执行时,事务大概率还没有真正提交。

而通过 TransactionSynchronization 注册的 afterCommit(),执行时机是在事务提交成功之后,因此更适合处理依赖已提交数据的逻辑。

适合哪些场景?

这个工具方法适合处理“必须等事务成功之后才能执行”的副作用逻辑。

典型场景包括:

1. 发送 MQ 消息

TransactionUtils.runAfterCommit(() -> {
    orderMessageProducer.sendOrderCreated(orderId);
});

避免数据库回滚但消息已经发出的情况。

2. 删除或刷新缓存

TransactionUtils.runAfterCommit(() -> {
    cacheManager.evict(userCacheKey);
});

避免事务尚未提交时缓存被提前刷新,导致其他线程读到旧数据或不一致数据。

3. 调用外部系统

TransactionUtils.runAfterCommit(() -> {
    externalApi.notifyStatusChanged(businessId);
});

外部系统一旦被调用,通常很难跟随本地事务一起回滚,因此更适合放在事务提交之后。

4. 发布领域事件

TransactionUtils.runAfterCommit(() -> {
    domainEventPublisher.publish(new UserRegisteredEvent(userId));
});

领域事件的消费者通常会基于数据库中的最新状态进行处理,因此事件发布时机最好晚于事务提交。

需要注意的问题

虽然这个工具方法很实用,但它并不是万能的。

1. afterCommit 中的异常不会回滚原事务

事务已经提交成功之后,afterCommit() 才会执行。

因此,如果 runnable.run() 抛出异常,原事务已经无法回滚。

例如:

TransactionUtils.runAfterCommit(() -> {
    mqProducer.send(message);
});

如果这里发送消息失败,数据库事务已经提交成功了。

所以,对于重要的后置逻辑,需要考虑失败补偿机制,例如:

  • 记录本地消息表;
  • 使用定时任务重试;
  • 引入可靠消息机制;
  • 对异常进行捕获和告警。

更稳妥的写法通常是:

TransactionUtils.runAfterCommit(() -> {
    try {
        mqProducer.send(message);
    } catch (Exception ex) {
        log.error("Send order created message failed, orderId={}", orderId, ex);
        // 记录失败状态,等待后续补偿
    }
});

2. 不适合承载太重的逻辑

afterCommit() 回调通常仍然发生在当前请求线程中。

如果在里面执行耗时逻辑,会拉长接口响应时间。

例如:

TransactionUtils.runAfterCommit(() -> {
    reportService.generateLargeReport();
});

这种逻辑就不适合直接放在 afterCommit() 中执行。

更合理的方式是:事务提交后只投递一个异步任务或消息,由后台消费者继续处理。

TransactionUtils.runAfterCommit(() -> {
    asyncTaskPublisher.publish(new GenerateReportTask(reportId));
});

3. 嵌套事务和传播行为需要谨慎理解

在复杂事务传播场景下,比如 REQUIRES_NEW、嵌套事务、多个事务边界交织时,afterCommit() 的触发时机取决于当前注册同步回调所绑定的事务上下文。

因此,在简单业务中使用该工具通常没有问题;但在复杂事务传播链路中,需要明确当前代码到底运行在哪个事务边界内。

4. 它不能替代分布式事务或可靠消息

这个工具方法解决的是“本地事务提交后再执行某段逻辑”的时机问题。

它不能保证:

  • MQ 一定发送成功;
  • 外部接口一定调用成功;
  • 下游系统一定消费成功;
  • 本地事务和远程系统操作具备原子性。

因此,对于强一致或高可靠场景,仍然需要使用更完整的方案,比如事务消息、本地消息表、Outbox Pattern 或 Saga 等。

可以进一步增强的版本

在实际项目中,可以对这个工具类做一些增强。

例如增加异常处理:

public static void runAfterCommit(Runnable runnable) {
    if (TransactionSynchronizationManager.isSynchronizationActive()
            && TransactionSynchronizationManager.isActualTransactionActive()) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                try {
                    runnable.run();
                } catch (Exception ex) {
                    // 这里可以统一记录日志
                    throw ex;
                }
            }
        });
        return;
    }
    runnable.run();
}

也可以支持自定义异常处理器:

public static void runAfterCommit(Runnable runnable, Consumer<Throwable> exceptionHandler) {
    Runnable safeRunnable = () -> {
        try {
            runnable.run();
        } catch (Throwable ex) {
            exceptionHandler.accept(ex);
        }
    };

    if (TransactionSynchronizationManager.isSynchronizationActive()
            && TransactionSynchronizationManager.isActualTransactionActive()) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                safeRunnable.run();
            }
        });
        return;
    }

    safeRunnable.run();
}

这样可以避免业务方每次都手动写 try-catch

小结

这个工具类虽然只有十几行代码,但它解决了一个非常常见的工程问题:如何保证某段副作用逻辑在事务成功提交之后再执行。

它的核心价值在于:

  • 避免事务回滚后外部副作用已经发生;
  • 统一事务内和非事务内的调用方式;
  • 降低业务代码对事务上下文的感知;
  • 让消息发送、缓存刷新、事件发布等逻辑拥有更准确的执行时机。

不过,它也需要配合正确的工程实践使用。

对于普通的事务后置逻辑,它简单、直接、够用。

对于涉及高可靠消息、跨系统一致性或复杂补偿的场景,则应该进一步结合本地消息表、事务消息、Outbox Pattern 等方案,构建更完整的可靠性机制。

一个好的工具方法,不一定要复杂。

有时候,十几行代码就能把一个隐蔽的时序问题封装得足够清晰。

使用 Hugo 构建
主题 StackJimmy 设计