Spring 事务提交后执行异步操作:原理、陷阱与最佳实践

Spring 事务提交后执行异步操作:原理、陷阱与最佳实践

一、为什么需要"事务提交后再做某事"

1.1 一个典型的错误场景

java 复制代码
@Transactional
public void createOrder(OrderDto orderDto) {
    // 1. 保存订单到数据库
    Order order = new Order();
    order.setOrderCode("ORD001");
    order.setStatus(0);
    orderRepository.save(order);

    // 2. 发送MQ通知下游系统处理这个订单
    mqSender.send(order.getId());

    // 3. 后续还有其他数据库操作...
    orderDetailRepository.save(detail);  // 假设这里抛异常了
}

问题:如果第3步抛异常,整个事务会回滚,数据库里不会有这条订单。但第2步的MQ消息已经发出去了!下游系统收到消息后去查订单,查不到,就会报错。

根本原因:MQ发送是不可回滚的操作,一旦发出就无法撤回。而数据库操作在事务内是可以回滚的。两者的"生效时机"不一致。

1.2 正确的做法

先让事务提交成功(数据确实写入了数据库),然后再发MQ。

这就是"事务提交后执行"模式的核心思想。


二、Spring 事务基础知识

2.1 什么是事务

事务是一组数据库操作的集合,要么全部成功(提交),要么全部失败(回滚)。

java 复制代码
@Transactional
public void transferMoney(Integer fromId, Integer toId, BigDecimal amount) {
    // 以下两步要么都成功,要么都不执行
    accountRepository.deduct(fromId, amount);   // 扣钱
    accountRepository.add(toId, amount);        // 加钱
}

2.2 @Transactional 注解

Spring 中通过 @Transactional 声明事务边界:

java 复制代码
@Transactional(
    propagation = Propagation.REQUIRED,    // 传播行为(默认值)
    rollbackFor = Exception.class          // 什么异常触发回滚
)
public void businessMethod() {
    // 这个方法内的所有数据库操作在同一个事务中
}

2.3 事务的生命周期

复制代码
方法调用开始
    │
    ▼
Spring 开启事务(BEGIN)
    │
    ▼
执行方法体中的代码(数据库操作在此执行)
    │
    ├── 正常结束 → 事务提交(COMMIT)→ 数据真正写入数据库
    │
    └── 抛出异常 → 事务回滚(ROLLBACK)→ 所有修改撤销

关键点:在方法体执行过程中,数据库的修改还没有真正"生效"。只有 COMMIT 之后,其他线程/系统才能看到这些数据。


三、事务提交后执行的实现方式

3.1 方式一:TransactionSynchronization(Spring 原生)

Spring 提供了 TransactionSynchronizationManager,允许你注册回调,在事务的不同阶段执行代码。

java 复制代码
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Transactional
public void createOrderAndNotify(OrderDto orderDto) {
    // 1. 数据库操作(在事务内)
    Order order = new Order();
    order.setOrderCode("ORD001");
    order.setStatus(0);
    orderRepository.save(order);

    // 2. 注册事务提交后的回调
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 这里的代码只在事务成功提交后才会执行
                mqSender.send(order.getId());
            }
        }
    );

    // 3. 继续其他数据库操作
    // 如果这里抛异常,事务回滚,MQ不会发送
    orderDetailRepository.save(detail);
}

3.2 方式二:TransactionSynchronizationAdapter(简化写法)

java 复制代码
import org.springframework.transaction.support.TransactionSynchronizationAdapter;

@Transactional
public void createOrderAndNotify(OrderDto orderDto) {
    Order order = orderRepository.save(buildOrder(orderDto));

    // 使用 Adapter 只需要重写需要的方法
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                mqSender.send(order.getId());
            }
        }
    );
}

注意:TransactionSynchronizationAdapter 在 Spring 5.3+ 已标记为废弃,推荐直接实现 TransactionSynchronization 接口(接口方法都有默认实现)。

3.3 方式三:封装工具类(项目中常用)

很多项目会封装一个工具类来简化使用:

java 复制代码
/**
 * 事务提交后执行动作的收集器.
 * 收集多个需要在事务提交后执行的操作,统一在 afterCommit 时触发.
 */
public class AfterTransactionActionCollector
        implements TransactionSynchronization {

    private final List<Runnable> commitActions = new ArrayList<>();
    private final List<Runnable> rollbackActions = new ArrayList<>();

    /**
     * 添加事务提交后执行的操作.
     */
    public void addCommitSyncAction(Runnable action) {
        commitActions.add(action);
    }

    /**
     * 添加事务回滚后执行的操作.
     */
    public void addRollbackSyncAction(Runnable action) {
        rollbackActions.add(action);
    }

    @Override
    public void afterCommit() {
        // 事务提交成功后,依次执行所有注册的操作
        for (Runnable action : commitActions) {
            try {
                action.run();
            } catch (Exception e) {
                // 记录日志但不抛异常,避免影响其他操作
                log.warn("事务提交后执行操作异常", e);
            }
        }
    }

    @Override
    public void afterCompletion(int status) {
        // 事务完成后(无论提交还是回滚)
        if (status == STATUS_ROLLED_BACK) {
            for (Runnable action : rollbackActions) {
                try {
                    action.run();
                } catch (Exception e) {
                    log.warn("事务回滚后执行操作异常", e);
                }
            }
        }
    }
}

使用方式

java 复制代码
@Transactional
public void processOrder(OrderDto orderDto) {
    // 数据库操作
    Order order = orderRepository.save(buildOrder(orderDto));

    // 创建收集器并注册多个事务后操作
    AfterTransactionActionCollector collector =
        new AfterTransactionActionCollector();

    // 操作1:发MQ通知下游
    collector.addCommitSyncAction(() -> {
        mqSender.send(order.getId());
    });

    // 操作2:发短信通知用户
    collector.addCommitSyncAction(() -> {
        smsSender.sendOrderConfirmSms(order.getPhone());
    });

    // 操作3:事务回滚时释放库存锁
    collector.addRollbackSyncAction(() -> {
        stockLockService.releaseLock(order.getSkuId());
    });

    // 注册到事务管理器
    TransactionSynchronizationManager.registerSynchronization(collector);
}

3.4 方式四:@TransactionalEventListener(Spring 4.2+)

基于事件机制的方式,代码解耦更好:

java 复制代码
// 1. 定义事件
public class OrderCreatedEvent {
    private final Integer orderId;

    public OrderCreatedEvent(Integer orderId) {
        this.orderId = orderId;
    }

    public Integer getOrderId() {
        return orderId;
    }
}

// 2. 在业务方法中发布事件
@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private ApplicationEventPublisher eventPublisher;

    @Transactional
    public void createOrder(OrderDto orderDto) {
        Order order = orderRepository.save(buildOrder(orderDto));

        // 发布事件(此时不会立即触发监听器)
        eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
    }
}

// 3. 监听器:事务提交后才执行
@Component
public class OrderEventListener {

    @Resource
    private MqSender mqSender;

    /**
     * 只在事务提交后才触发.
     * phase = AFTER_COMMIT 是关键配置.
     */
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderCreated(OrderCreatedEvent event) {
        mqSender.send(event.getOrderId());
    }
}

四、TransactionSynchronization 回调时机详解

TransactionSynchronization 接口提供了多个回调方法,对应事务生命周期的不同阶段:

java 复制代码
public interface TransactionSynchronization {

    // 事务提交之前调用(还可以抛异常阻止提交)
    default void beforeCommit(boolean readOnly) {}

    // 事务完成之前调用(提交或回滚之前)
    default void beforeCompletion() {}

    // 事务成功提交之后调用
    default void afterCommit() {}

    // 事务完成之后调用(无论提交还是回滚)
    // status: STATUS_COMMITTED(0) 或 STATUS_ROLLED_BACK(1)
    default void afterCompletion(int status) {}
}

执行顺序

复制代码
事务开始
    │
    ▼
执行业务代码
    │
    ▼
beforeCommit()     ← 提交前,可以做最后的校验
    │
    ▼
beforeCompletion() ← 完成前
    │
    ▼
数据库 COMMIT(或 ROLLBACK)
    │
    ├── 提交成功 → afterCommit()       ← 发MQ、发通知等
    │                │
    │                ▼
    │           afterCompletion(0)      ← 清理资源
    │
    └── 回滚 → afterCompletion(1)      ← 释放锁、补偿操作

五、常见陷阱与注意事项

5.1 afterCommit 中抛异常不会回滚事务

java 复制代码
TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            // 即使这里抛异常,事务已经提交了,不会回滚!
            mqSender.send(orderId); // 如果MQ发送失败...
        }
    }
);

解决方案:在 afterCommit 中用 try-catch 包裹,记录日志,后续通过补偿机制处理。

java 复制代码
@Override
public void afterCommit() {
    try {
        mqSender.send(orderId);
    } catch (Exception e) {
        // 记录日志,由定时任务补偿重试
        log.warn("事务提交后发送MQ失败, orderId:{}", orderId, e);
    }
}

5.2 必须在事务上下文中注册

java 复制代码
// 错误:没有 @Transactional,注册会失败
public void noTransactionMethod() {
    // 此时没有活跃事务,注册无效!
    TransactionSynchronizationManager.registerSynchronization(...);
}

如果不确定当前是否有事务,可以先检查:

java 复制代码
if (TransactionSynchronizationManager.isSynchronizationActive()) {
    TransactionSynchronizationManager.registerSynchronization(...);
} else {
    // 没有事务,直接执行
    mqSender.send(orderId);
}

5.3 afterCommit 中不能再做数据库写操作

java 复制代码
@Override
public void afterCommit() {
    // 危险!此时事务已经提交完毕,这里的数据库操作不在原事务中
    // 如果需要写数据库,必须开启新事务
    orderRepository.updateStatus(orderId, "NOTIFIED");
}

正确做法 :如果 afterCommit 中需要写数据库,调用一个带 @Transactional(propagation = Propagation.REQUIRES_NEW) 的方法。

5.4 Lambda 中引用的变量必须是 able to be final

java 复制代码
@Transactional
public void processOrder(OrderDto orderDto) {
    Order order = orderRepository.save(buildOrder(orderDto));

    // order 变量在 lambda 中被引用,不能再被重新赋值
    Integer orderId = order.getId(); // 用局部变量接收

    AfterTransactionActionCollector collector =
        new AfterTransactionActionCollector();
    collector.addCommitSyncAction(() -> {
        mqSender.send(orderId); // 引用局部变量
    });
    TransactionSynchronizationManager.registerSynchronization(collector);
}

5.5 嵌套事务中的行为

java 复制代码
@Transactional
public void outerMethod() {
    // 注册回调
    TransactionSynchronizationManager.registerSynchronization(...);

    // 调用内部方法(默认 REQUIRED 传播,共享同一事务)
    innerMethod();
}

@Transactional
public void innerMethod() {
    // 这里注册的回调也会在外层事务提交后执行
    TransactionSynchronizationManager.registerSynchronization(...);
}

回调是绑定在最外层事务上的。只有最外层事务提交时,所有注册的回调才会执行。


六、四种方式对比

方式 适用场景 优点 缺点
TransactionSynchronization 通用场景 灵活,可控制多个阶段 代码稍显冗长
AfterTransactionActionCollector 需要注册多个操作 收集多个操作,统一管理 需要自定义工具类
@TransactionalEventListener 事件驱动架构 解耦好,代码清晰 需要定义事件类
手动提交事务后执行 编程式事务 完全控制 不适合声明式事务

七、完整示例:订单创建后通知多个下游系统

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderRepository orderRepository;

    @Resource
    private OrderDetailRepository orderDetailRepository;

    @Resource
    private OrderMqSender orderMqSender;

    @Resource
    private SmsSender smsSender;

    /**
     * 创建订单并通知下游.
     * 数据库操作在事务内,MQ和短信在事务提交后发送.
     */
    @Transactional(rollbackFor = Exception.class)
    public Integer createOrder(CreateOrderParamsDto paramsDto) {
        // ====== 事务内:数据库操作 ======

        // 保存订单主表
        Order order = new Order();
        order.setOrderCode(generateOrderCode());
        order.setCustomerPhone(paramsDto.getPhone());
        order.setStatus(0); // 待处理
        order.setAmount(paramsDto.getTotalAmount());
        orderRepository.saveAndFlush(order);

        // 保存订单明细
        for (OrderItemDto item : paramsDto.getItems()) {
            OrderDetail detail = new OrderDetail();
            detail.setOrderId(order.getId());
            detail.setSkuId(item.getSkuId());
            detail.setQuantity(item.getQuantity());
            orderDetailRepository.save(detail);
        }

        // ====== 注册事务提交后的操作 ======
        Integer orderId = order.getId();
        String phone = order.getCustomerPhone();

        AfterTransactionActionCollector collector =
            new AfterTransactionActionCollector();

        // 事务提交后:发MQ通知仓库系统
        collector.addCommitSyncAction(() -> {
            try {
                orderMqSender.sendOrderCreatedMq(orderId);
            } catch (Exception e) {
                log.warn("订单创建MQ发送失败, orderId:{}", orderId, e);
            }
        });

        // 事务提交后:发短信通知客户
        collector.addCommitSyncAction(() -> {
            try {
                smsSender.sendOrderConfirmSms(phone, orderId);
            } catch (Exception e) {
                log.warn("订单确认短信发送失败, phone:{}", phone, e);
            }
        });

        TransactionSynchronizationManager.registerSynchronization(collector);

        return orderId;
    }

    private String generateOrderCode() {
        return "ORD" + System.currentTimeMillis();
    }
}

执行流程

复制代码
1. Spring 开启事务
2. 保存订单主表 → 数据库(未提交,其他线程看不到)
3. 保存订单明细 → 数据库(未提交)
4. 注册 afterCommit 回调(只是注册,不执行)
5. 方法正常返回
6. Spring 提交事务 → COMMIT → 数据真正写入数据库
7. 触发 afterCommit → 发MQ → 发短信
   └── 此时下游系统收到MQ后查询订单,一定能查到(因为已经COMMIT了)

如果第3步抛异常

复制代码
1. Spring 开启事务
2. 保存订单主表 → 数据库(未提交)
3. 保存订单明细 → 抛异常!
4. Spring 回滚事务 → ROLLBACK → 订单主表的数据也撤销
5. afterCommit 不会被触发 → MQ不会发送 → 短信不会发送
   └── 数据一致性得到保证

八、总结

问题 答案
什么时候用这个模式? 事务内产生了需要通知外部系统的数据,且外部通知不可回滚时
核心原理是什么? 利用 Spring 事务同步机制,在 COMMIT 成功后才执行副作用操作
如果 afterCommit 失败怎么办? 记录日志 + 定时任务补偿重试(查询 status=0 的记录重新推送)
和直接在方法最后一行发MQ有什么区别? 方法最后一行执行时事务还没提交,如果提交阶段失败,MQ已经发了但数据没写入
性能影响大吗? 几乎没有,只是注册了一个回调对象,不涉及额外IO
相关推荐
无小道2 小时前
Redis——list相关指令
数据库·redis·缓存
阳光九叶草LXGZXJ2 小时前
达梦数据库-堆栈看问题-01-asmapi_asm_extent_load
linux·运维·数据库·sql·学习
你的保护色2 小时前
ensp之STP、RSTP、MSTP协议实验
java·服务器·数据库
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
获取容器mysql管理员密码命令
数据库·mysql
JAVA学习通2 小时前
《大营销平台系统设计实现》 - 营销服务 第5节:抽奖前置规则过滤
java·数据库·github
斯特凡今天也很帅2 小时前
新建数据源报错No bean named ‘SqlSessionFactorykf‘ available
java·数据库·spring boot·mybatis
Trouvaille ~3 小时前
【Redis篇】为什么需要 Redis:从单机到分布式的架构演进之路
数据库·redis·分布式·缓存·中间件·架构·后端开发
ID_180079054733 小时前
Taobao & 1688 Product API Technical Overview and JSON Response Reference
数据库
TheRouter3 小时前
PromptCaching 工程实践:把LLM 调用成本砍掉80%
java·后端·spring·ai