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 |