景德镇市网站建设_网站建设公司_数据统计_seo优化
2025/12/31 13:29:25 网站建设 项目流程

@Transactional做不到的5件事,我用这6种方法解决了

看Mall项目订单代码时发现:一个方法操作6张表,14步业务逻辑,全在一个事务里,居然没炸。

研究了两天,发现了6种比@Transactional更灵活的玩法。写了个demo项目验证了一遍。

我们要解决的痛点

日常开发中,@Transactional解决不了的几个问题:

  1. 库存不足时:想保留订单记录标记"待补货",但不知道怎么不回滚
  2. 发MQ消息:在事务里发了消息,结果事务回滚了,消息却发出去了
  3. 批量操作:100个订单发货,1个失败就全部回滚,但其实想让成功的继续
  4. 记录日志:业务失败了也想记录日志,但事务回滚了日志也没了
  5. 隔离级别/超时:不知道@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

回滚事务

订单被删除

这才是编程式事务的价值
场景@TransactionalTransactionTemplate
风控不通过抛异常→全回滚不回滚,保留订单
风控服务挂了抛异常→全回滚回滚,不留脏数据
库存不足抛异常→全回滚保留订单,标记"待补货"

核心区别:能区分"业务失败"和"系统异常",动态决定要不要回滚。

我测试了一下:

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吗?

理论上,如果日志表和订单表在同一个数据库、同一个事务里,写早了会一起回滚,不会有问题。

但实际业务中,我们希望:

  1. 解耦:订单业务和日志记录分离
  2. 性能:日志操作不影响主事务耗时
  3. 重试:日志失败可以独立重试,不影响订单

所以建议还是放在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种玩法,每个都能解决实际问题:

  1. 编程式事务→ 库存不足保留订单
  2. @Transactional参数→ 隔离级别、超时、回滚规则
  3. 事务同步器→ 事务提交后发MQ
  4. 事务事件监听→ 解耦业务逻辑
  5. 手动控制事务→ 批量操作
  6. 事务传播机制→ 日志记录、赠品创建

80%的场景,@Transactional就够了。遇到特殊情况,再用对应的高级用法。

别过度设计,够用就行。

代码在这里

所有代码都是可以跑的,有完整测试用例。

数据库脚本在 doc/simple-transactional-init.sql,导入就能用。


你们平时用Spring事务都遇到过什么坑?

或者有什么好的实践经验?

欢迎在评论区聊聊,我也想学习学习。

特别是事务传播机制那块,我自己还没完全搞透。如果有大佬愿意指点一下,那就太好了。


如果这篇文章对你有帮助,麻烦点个赞👍,让更多人看到。

这篇文章从研究Mall源码到写demo,再到写文章、画图、测试,前后花了两天时间。

希望能帮你解决实际问题

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询