@Transactional做不到的5件事,我用这6种方法解决了
看Mall项目订单代码时发现:一个方法操作6张表,14步业务逻辑,全在一个事务里,居然没炸。
研究了两天,发现了6种比@Transactional更灵活的玩法。写了个demo项目验证了一遍。
我们要解决的痛点
日常开发中,@Transactional解决不了的几个问题:
- 库存不足时:想保留订单记录标记"待补货",但不知道怎么不回滚
- 发MQ消息:在事务里发了消息,结果事务回滚了,消息却发出去了
- 批量操作:100个订单发货,1个失败就全部回滚,但其实想让成功的继续
- 记录日志:业务失败了也想记录日志,但事务回滚了日志也没了
- 隔离级别/超时:不知道
@Transactional那些参数怎么用
这篇文章会用实际代码演示6种解决方案。
目录
- 编程式事务:区分业务失败和系统异常
- @Transactional参数:隔离级别和超时
- 事务同步器:提交后发MQ
- 事务事件监听:解耦副作用操作
- 手动控制事务:批量操作
- 事务传播机制:3种常用场景
编程式事务:区分业务失败和系统异常
这是我在Mall里发现的一个场景:订单创建后要调用风控服务检查。
- 风控不通过(业务规则):订单要保留,标记"待审核",人工复核
- 风控服务挂了(系统故障):订单要回滚,不能留脏数据
用@Transactional做不到。因为它只能靠抛异常触发回滚,无法区分这两种情况。
TransactionTemplate可以动态控制
java
体验AI代码助手
代码解读
复制代码
public OrderResult createOrder(OrderParam param) { return transactionTemplate.execute(status -> { try { // 1. 创建订单 Order order = buildOrder(param); orderMapper.insert(order); // 2. 创建订单商品 List<OrderItem> items = buildOrderItems(order); orderItemMapper.batchInsert(items); // 3. 锁定库存 lockStock(param.getItems()); // 4. 调用风控服务检查 RiskCheckResult riskResult = riskService.check(order); if (!riskResult.isPass()) { // 风控不通过 - 业务失败,但不回滚 order.setStatus(OrderStatus.WAIT_AUDIT); // 待审核 order.setNote("风控检查未通过:" + riskResult.getReason()); orderMapper.updateById(order); // 关键:不调用 status.setRollbackOnly() // 订单和商品明细都会保留 return OrderResult.fail("订单需人工审核"); } // 风控通过,订单正常 return OrderResult.success(order.getId()); } catch (RiskServiceException e) { // 风控服务异常 - 系统故障,必须回滚 log.error("风控服务异常", e); status.setRollbackOnly(); return OrderResult.error("系统异常,请稍后重试"); } catch (Exception e) { // 其他异常也回滚 status.setRollbackOnly(); return OrderResult.error(e.getMessage()); } }); }
画个图就明白了
不通过-业务失败
通过
服务异常-系统故障
开始事务
创建订单
创建订单商品
锁定库存
调用风控服务
风控结果?
更新订单状态=待审核
提交事务
订单保留,状态=待审核
提交事务
订单正常创建
setRollbackOnly
回滚事务
订单被删除
这才是编程式事务的价值
| 场景 | @Transactional | TransactionTemplate |
|---|---|---|
| 风控不通过 | 抛异常→全回滚 | 不回滚,保留订单 |
| 风控服务挂了 | 抛异常→全回滚 | 回滚,不留脏数据 |
| 库存不足 | 抛异常→全回滚 | 保留订单,标记"待补货" |
核心区别:能区分"业务失败"和"系统异常",动态决定要不要回滚。
我测试了一下:
bash
体验AI代码助手
代码解读
复制代码
# 测试风控不通过(高金额订单) POST /programmatic/risk-check # 结果 订单ID:8 订单状态:待审核 订单备注:风控检查未通过:金额过高 数据库:订单和商品明细都保留了
这玩意儿我之前真不知道能这么用。
@Transactional的参数,我被坑过
Mall的商品创建方法是这么写的:
java
体验AI代码助手
代码解读
复制代码
@Transactional( isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED, timeout = 30, rollbackFor = Exception.class ) public int createProduct(ProductParam param) { // 插入8张表... }
我之前都是直接@Transactional,从来不加参数。后来踩了几次坑才知道这些参数的用处。
isolation这个参数要注意
有次数据库从MySQL换成PostgreSQL,突然出现了幻读问题。
原因是:
- MySQL默认
REPEATABLE_READ(可重复读) - PostgreSQL默认
READ_COMMITTED(读已提交)
如果代码里没显式指定隔离级别,换数据库就可能出问题。
所以建议:
java
体验AI代码助手
代码解读
复制代码
// 明确指定隔离级别,不依赖数据库默认值 @Transactional(isolation = Isolation.REPEATABLE_READ) public void someMethod() { // 避免环境切换导致行为变化 }
timeout和rollbackFor简单说两句
timeout:防止长事务锁表
java
体验AI代码助手
代码解读
复制代码
@Transactional(timeout = 30) // 30秒超时 public void complexTask() { // ... }
rollbackFor:Spring默认只有RuntimeException才回滚,Checked Exception不回滚
java
体验AI代码助手
代码解读
复制代码
@Transactional(rollbackFor = Exception.class) // 明确指定 public void createOrder() throws Exception { // ... }
这两个参数记得加上,能避免很多坑。
事务提交后发MQ,我之前都做错了
订单创建成功后,要发个MQ消息(30分钟后自动取消未支付订单)。
我之前是这么写的:
java
体验AI代码助手
代码解读
复制代码
@Transactional public void createOrder() { orderMapper.insert(order); // 直接发MQ mqSender.send("order.cancel.delay", order.getId()); }
看起来没问题吧?实际上有个致命问题。
问题出在时机上
画个图就明白了:
ServiceDatabaseRabbitMQ错误的做法数据在事务内还没提交消息已发出订单被删除问题:消息发了但数据没了1. INSERT订单2. 发送MQ消息3. 后面某步失败4. 事务回滚ServiceDatabaseRabbitMQ
问题本质:MQ消息发出去了,但事务回滚了,订单根本不存在。30分钟后消费者去取消订单,发现订单不存在。
这就是副作用的时机与事务一致性问题:
- 订单插入、库存扣减 → 在同一个事务里,要么全成功,要么全回滚
- MQ消息 → 不在这个事务里,发出去就收不回来了
事务同步器解决这个问题
Spring提供了事务生命周期的钩子,让你在特定阶段执行回调:
java
体验AI代码助手
代码解读
复制代码
@Transactional public void createOrder() { orderMapper.insert(order); // 注册事务同步器 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { // 只有事务提交成功,这里才会执行 mqSender.send("order.cancel.delay", order.getId()); log.info("MQ消息已发送"); } @Override public void afterCompletion(int status) { if (status == STATUS_ROLLED_BACK) { log.info("事务回滚,MQ消息不会发送"); } } } ); }
现在的时序是这样:
ServiceDatabaseRabbitMQ正确的做法数据已持久化数据和消息一致数据被删除afterCommit不执行消息不会发送alt[事务成功][事务失败]1. INSERT订单2. 注册afterCommit回调3. COMMIT4. 触发afterCommit发送MQ消息3. ROLLBACKServiceDatabaseRabbitMQ
核心区别:只有订单真正提交到数据库后,才发MQ消息。事务回滚了,消息就不发。
4个生命周期钩子
事务同步器提供了4个回调点:
java
体验AI代码助手
代码解读
复制代码
TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void beforeCommit(boolean readOnly) { log.info("【阶段1-beforeCommit】事务即将提交"); // 最后的数据校验 } @Override public void beforeCompletion() { log.info("【阶段2-beforeCompletion】事务即将完成"); // 清理临时资源 } @Override public void afterCommit() { log.info("【阶段3-afterCommit】事务已提交"); // 发MQ、清缓存(数据已持久化) } @Override public void afterCompletion(int status) { String statusStr = (status == STATUS_COMMITTED) ? "提交" : "回滚"; log.info("【阶段4-afterCompletion】事务已完成,状态:{}", statusStr); } } );
执行顺序是固定的:
是
否
开始事务
业务逻辑执行
要提交?
beforeCommit
beforeCompletion
COMMIT
afterCommit
afterCompletion状态COMMITTED
beforeCompletion
ROLLBACK
afterCompletion状态ROLLED_BACK
哪些场景必须用afterCommit
所有"对外的副作用"都应该放在afterCommit里:
场景1:发MQ消息
java
体验AI代码助手
代码解读
复制代码
@Override public void afterCommit() { // 延迟取消订单 mqSender.send("order.cancel.delay", orderId); }
场景2:清理缓存
java
体验AI代码助手
代码解读
复制代码
@Override public void afterCommit() { // 清理商品缓存 redisTemplate.delete("product:" + productId); }
场景3:记录日志到另一个库
java
体验AI代码助手
代码解读
复制代码
@Override public void afterCommit() { // 写到日志库(不在当前事务) logMapper.insert(businessLog); }
场景4:调用外部服务
java
体验AI代码助手
代码解读
复制代码
@Override public void afterCommit() { // 通知第三方 thirdPartyService.notify(order); }
核心原则:只有订单数据真正持久化了,外部世界才能知道。
同库的日志也要用afterCommit吗?
理论上,如果日志表和订单表在同一个数据库、同一个事务里,写早了会一起回滚,不会有问题。
但实际业务中,我们希望:
- 解耦:订单业务和日志记录分离
- 性能:日志操作不影响主事务耗时
- 重试:日志失败可以独立重试,不影响订单
所以建议还是放在afterCommit里。
我测试了一下
bash
体验AI代码助手
代码解读
复制代码
# 运行测试 POST /synchronization/phases # 控制台输出 【阶段1-beforeCommit】事务即将提交 【阶段2-beforeCompletion】事务即将完成 【阶段3-afterCommit】事务已提交 【阶段4-afterCompletion】事务已完成,状态:提交 MQ消息已发送
顺序是固定的,非常可靠。
事务事件监听:解耦副作用操作
订单创建成功后,要做3件事:发MQ、记录日志、发通知。
如果都写在一个方法里,代码会很臃肿:
java
体验AI代码助手
代码解读
复制代码
@Transactional public void createOrder() { orderMapper.insert(order); // 业务逻辑越来越多 mqSender.send(...); logService.save(...); notifyService.send(...); }
而且事务范围太大了,发短信也在事务里?
事务事件监听可以解耦
第1步:定义事件
java
体验AI代码助手
代码解读
复制代码
@Getter @AllArgsConstructor public class OrderCreatedEvent { private String orderSn; private Long memberId; private BigDecimal amount; }
第2步:发布事件
java
体验AI代码助手
代码解读
复制代码
@Service public class OrderService { @Autowired private ApplicationEventPublisher eventPublisher; @Transactional public void createOrder(OrderParam param) { // 创建订单 orderMapper.insert(order); // 发布事件(立即发布,但监听器何时处理取决于监听方式) OrderCreatedEvent event = new OrderCreatedEvent( order.getOrderSn(), order.getMemberId(), order.getTotalAmount() ); eventPublisher.publishEvent(event); log.info("事件已发布"); } }
第3步:监听事件
java
体验AI代码助手
代码解读
复制代码
@Component public class OrderEventListener { // 关键:@TransactionalEventListener + AFTER_COMMIT // 事件会被"挂起",等事务提交成功后才处理 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleOrderCreated(OrderCreatedEvent event) { log.info("监听到订单创建:{}", event.getOrderSn()); // 这些操作在事务提交后才执行 mqSender.send("order.cancel", event.getOrderSn()); logMapper.insert(log); notifyService.send(event.getMemberId()); } // 事务回滚后执行 @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) public void handleOrderFailed(OrderCreatedEvent event) { log.info("订单创建失败:{}", event.getOrderSn()); } }
事件发布与事务的关系
这里容易混淆的点:publishEvent本身与事务无关,但监听器的执行时机取决于监听方式。
画个图说明:
OrderServiceEventPublisher@EventListener普通监听器@TransactionalEventListenerAFTER_COMMITDatabase方法有 @Transactional普通监听器立即执行此时事务还没提交AFTER_COMMIT监听器不执行等待事务提交监听器执行数据已持久化AFTER_COMMIT不执行alt[事务提交成功][事务回滚]1. INSERT订单2. publishEvent(event)3. 立即同步调用4. 事件挂起5. COMMIT6. 触发AFTER_COMMIT5. ROLLBACKOrderServiceEventPublisher@EventListener普通监听器@TransactionalEventListenerAFTER_COMMITDatabase
关键区别:
| 监听方式 | 执行时机 | 事务回滚影响 |
|---|---|---|
| @EventListener | 立即执行 | 已执行的副作用无法撤销 |
| @TransactionalEventListener(AFTER_COMMIT) | 事务提交后 | 事务回滚则不执行 |
| @TransactionalEventListener(AFTER_ROLLBACK) | 事务回滚后 | 只有回滚才执行 |
4个事务阶段
java
体验AI代码助手
代码解读
复制代码
// 提交前(做最后校验) @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) public void beforeCommit(OrderCreatedEvent event) { // 事务即将提交,可以做最后校验 } // 提交后(发副作用) @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void afterCommit(OrderCreatedEvent event) { // 数据已持久化,可以安全地发MQ、清缓存 } // 回滚后(记录失败) @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) public void afterRollback(OrderCreatedEvent event) { // 事务失败了,记录失败日志 } // 完成后(清理资源) @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION) public void afterCompletion(OrderCreatedEvent event) { // 无论成功失败都会执行 }
几个要注意的地方
1. 必须在事务方法里发布
java
体验AI代码助手
代码解读
复制代码
// 错误:方法没有 @Transactional public void createOrder() { orderMapper.insert(order); eventPublisher.publishEvent(event); // AFTER_COMMIT监听器不会触发! } // 正确:方法有 @Transactional @Transactional public void createOrder() { orderMapper.insert(order); eventPublisher.publishEvent(event); // 监听器会在提交后触发 }
2. 子事务的事件跟随子事务
java
体验AI代码助手
代码解读
复制代码
@Transactional public void parentMethod() { // 父事务 childMethod(); // 子事务(REQUIRES_NEW) } @Transactional(propagation = Propagation.REQUIRES_NEW) public void childMethod() { orderMapper.insert(order); eventPublisher.publishEvent(event); // 监听器跟随子事务的提交 }
3. 如果没有事务怎么办
java
体验AI代码助手
代码解读
复制代码
// 监听器默认不执行,除非加 fallbackExecution=true @TransactionalEventListener( phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true // 没有事务也会执行 ) public void handleEvent(OrderCreatedEvent event) { // ... }
对比事务同步器
| 方式 | 代码耦合度 | 扩展性 | 适用场景 |
|---|---|---|---|
| TransactionSynchronization | 高(在方法里注册) | 低 | 简单场景,1-2个操作 |
| @TransactionalEventListener | 低(发布订阅) | 高 | 复杂场景,多个操作 |
我的建议:
- 只有1-2个操作,用TransactionSynchronization
- 有多个操作,或者可能扩展,用@TransactionalEventListener
好处是代码解耦了,要加新功能,写个监听器就行,不用改原方法。
批量操作必须用手动事务
批量发货100个订单,其中1个失败了咋办?
如果用@Transactional:
java
体验AI代码助手
代码解读
复制代码
@Transactional public void batchDelivery(List<Long> orderIds) { for (Long orderId : orderIds) { // 发货逻辑 } }
问题:100个订单在一个事务里,1个失败全部回滚。
但实际需求是:成功的正常发货,失败的记录下来。
用PlatformTransactionManager手动控制
java
体验AI代码助手
代码解读
复制代码
@Service public class OrderBatchService { @Autowired private PlatformTransactionManager transactionManager; public BatchResult batchDelivery(List<Long> orderIds) { List<Long> success = new ArrayList<>(); List<String> failed = new ArrayList<>(); for (Long orderId : orderIds) { // 每个订单一个独立事务 DefaultTransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(def); try { // 发货逻辑 Order order = orderMapper.selectById(orderId); order.setStatus(2); // 已发货 orderMapper.updateById(order); reduceStock(order); // 手动提交 transactionManager.commit(status); success.add(orderId); } catch (Exception e) { // 手动回滚 transactionManager.rollback(status); failed.add("订单" + orderId + ":" + e.getMessage()); } } return new BatchResult(success, failed); } }
高级用法:设置事务属性
对于定时任务、后台批处理这种场景,可以显式控制事务属性:
java
体验AI代码助手
代码解读
复制代码
public BatchResult batchCloseOrder(List<Long> orderIds) { for (Long orderId : orderIds) { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // 强制新事务(无论外层是否有事务) def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); // 降低隔离级别,减少锁争用 def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); // 设置超时,避免长事务阻塞 def.setTimeout(10); TransactionStatus status = transactionManager.getTransaction(def); try { Order order = orderMapper.selectById(orderId); order.setStatus(4); // 已关闭 orderMapper.updateById(order); transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); } } }
三种方式对比
| 方式 | 事务范围 | 一条失败影响 | 适用场景 |
|---|---|---|---|
| @Transactional | 整个批次 | 全部回滚 | 不适合批量 |
| 手动事务(默认属性) | 每条独立 | 只回滚这条 | 普通批处理 |
| 手动事务(定制属性) | 每条独立 | 只回滚这条 | 高并发批处理 |
我测试了100个订单,97个成功,3个失败。成功的都发货了,失败的记录下来了。
核心价值:每条数据独立事务,部分失败不影响其他。
事务传播机制:3种常用场景
创建订单时,要调另一个方法插入订单商品。两个方法都有@Transactional,会咋样?
7种传播机制,常用的是3种:REQUIRED、REQUIRES_NEW、NESTED。
REQUIRED(默认):同成同败
行为:有事务就加入,没有就新建。父子方法共享同一个事务。
java
体验AI代码助手
代码解读
复制代码
// 父方法 @Transactional(propagation = Propagation.REQUIRED) public void createOrder() { orderMapper.insert(order); createOrderItems(order.getId()); // 加入当前事务 } // 子方法 @Transactional(propagation = Propagation.REQUIRED) public void createOrderItems(Long orderId) { itemMapper.batchInsert(items); }
关键点:
- 父子方法在同一个事务里
- 子方法抛异常 → 整个事务回滚(父也一起回滚)
- 订单和订单商品"同成同败"
是
否
createOrder开启事务
insert order
createOrderItems加入事务
insert items
子方法异常?
整个事务回滚
事务提交
适用场景:一个业务流程内的多步骤需要"同成同败"。80%的场景都用这个。
REQUIRES_NEW:独立事务
行为:挂起当前事务,开启一个全新的事务,独立提交/回滚。
java
体验AI代码助手
代码解读
复制代码
// 父方法 @Transactional public void createOrder() { orderMapper.insert(order); logService.saveLog(log); // 新事务,独立提交 // 后面的代码可能失败 } // 子方法 @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveLog(Log log) { logMapper.insert(log); }
关键点:
- 子方法失败只影响子事务,父事务不受影响
- 父事务后续回滚,子事务已提交的结果也保留
- 日志一定会保存,即使订单创建失败
成功
失败
createOrder事务1
insert order
挂起事务1
saveLog开启事务2
insert log
事务2提交-日志已保存
恢复事务1
事务1继续执行
事务1提交
事务1回滚-但日志保留
适用场景:必须独立持久化的动作,如:
- 记录审计日志
- 写消息表
- 发送通知记录
即使主流程失败也不能丢。
NESTED:局部回滚
行为:在同一物理事务内使用"保存点"(Savepoint),子方法相当于子事务。
java
体验AI代码助手
代码解读
复制代码
// 父方法 @Transactional public void createOrder() { orderMapper.insert(order); try { createGift(order.getId()); // 嵌套事务 } catch (Exception e) { // 赠品创建失败,但订单继续 log.warn("赠品创建失败,继续处理订单"); } // 订单正常提交 } // 子方法 @Transactional(propagation = Propagation.NESTED) public void createGift(Long orderId) { giftMapper.insert(gift); }
关键点:
- 子方法回滚只回滚到保存点,不影响父方法已做的操作
- 父方法回滚会连同子方法一起回滚
- 需要数据库支持保存点(InnoDB支持)
失败
成功
createOrder事务开启
insert order
创建保存点
createGift嵌套事务
赠品创建
回滚到保存点
订单保留
事务提交
继续执行
适用场景:主流程可继续,但某个子步骤允许"局部失败回滚"。
- 批处理中某条失败不影响前面已写入的步骤
- 赠品、优惠券等可选功能
注意:
- 需要使用
DataSourceTransactionManager(JPA的不支持) - 数据库必须支持保存点(InnoDB支持,MyISAM不支持)
三种传播行为对比
| 传播行为 | 事务关系 | 子方法失败影响 | 父方法失败影响 | 典型场景 |
|---|---|---|---|---|
| REQUIRED | 共享事务 | 整个事务回滚 | 整个事务回滚 | 订单+订单商品 |
| REQUIRES_NEW | 独立事务 | 只回滚子事务 | 子事务已提交 | 审计日志 |
| NESTED | 保存点 | 回滚到保存点 | 整个事务回滚 | 赠品、优惠券 |
选型建议
- 默认用REQUIRED:80%的场景都是"同成同败"
- 需要独立落盘的用REQUIRES_NEW:审计日志、消息表
- 需要局部回滚的用NESTED:可选功能、批处理
我测试了一下,这3种传播行为都符合预期。
几个要注意的地方
事务范围要小
java
体验AI代码助手
代码解读
复制代码
// 不好的写法 @Transactional public void process() { List<Data> data = queryBigData(); // 慢查询,不需要事务 Data result = calculate(data); // 计算,不需要事务 mapper.save(result); // 真正需要事务 } // 改成这样 public void process() { List<Data> data = queryBigData(); Data result = calculate(data); saveInTransaction(result); } @Transactional private void saveInTransaction(Data data) { mapper.save(data); }
只把写操作放事务里。
批量插入要用batchInsert
java
体验AI代码助手
代码解读
复制代码
// 慢 @Transactional public void save(List<Item> items) { for (Item item : items) { mapper.insert(item); // N次数据库访问 } } // 快 @Transactional public void save(List<Item> items) { mapper.batchInsert(items); // 1次数据库访问 }
我之前不知道这个,踩过坑。1000条数据,循环插入要10秒,批量插入只要0.5秒。
长事务要设置超时
java
体验AI代码助手
代码解读
复制代码
@Transactional(timeout = 30) public void longTask() { // 防止锁表 }
生产环境一定要加这个。
总结一下
这6种玩法,每个都能解决实际问题:
- 编程式事务→ 库存不足保留订单
- @Transactional参数→ 隔离级别、超时、回滚规则
- 事务同步器→ 事务提交后发MQ
- 事务事件监听→ 解耦业务逻辑
- 手动控制事务→ 批量操作
- 事务传播机制→ 日志记录、赠品创建
80%的场景,@Transactional就够了。遇到特殊情况,再用对应的高级用法。
别过度设计,够用就行。
代码在这里
所有代码都是可以跑的,有完整测试用例。
数据库脚本在 doc/simple-transactional-init.sql,导入就能用。
你们平时用Spring事务都遇到过什么坑?
或者有什么好的实践经验?
欢迎在评论区聊聊,我也想学习学习。
特别是事务传播机制那块,我自己还没完全搞透。如果有大佬愿意指点一下,那就太好了。
如果这篇文章对你有帮助,麻烦点个赞👍,让更多人看到。
这篇文章从研究Mall源码到写demo,再到写文章、画图、测试,前后花了两天时间。
希望能帮你解决实际问题