在 Spring Boot 中,所谓的“事务嵌套”通常是指在一个事务方法中调用另一个事务方法。Spring 通过**事务传播机制(Propagation Behavior)**来处理这种情况,而不是像数据库那样通过SAVEPOINT来实现真正的子事务。
要正确处理事务嵌套,关键在于理解并选择合适的传播行为,最常用的是REQUIRED和REQUIRES_NEW。
📌 结论
| 传播行为 | 行为描述 | 适用场景 |
|---|---|---|
| REQUIRED | 加入当前事务,无事务则新建 | 默认选择,业务逻辑强关联,要求整体一致性。 |
| REQUIRES_NEW | 挂起当前事务,创建新事务 | 独立操作,如记日志、发消息,不希望受主事务回滚影响。 |
| NESTED | 在当前事务中创建一个保存点 | 复杂业务,需要精确控制部分回滚,但需注意数据库支持。 |
📥 REQUIRED (默认行为)
这是最常用的传播行为。它遵循以下规则:
- 如果当前存在一个事务,则加入该事务。
- 如果当前没有事务,则创建一个新事务。
在这种模式下,所有嵌套调用的方法都处于同一个事务中。只要其中任意一个方法抛出了未被捕获的异常,整个事务都会被标记为回滚,所有数据库操作都将失效。
适用场景:业务逻辑强关联,要求所有操作要么全部成功,要么全部失败。
@ServicepublicclassUserService{@AutowiredprivateOrderServiceorderService;@TransactionalpublicvoidcreateUserAndOrder(Useruser){// 1. 保存用户 (在当前事务中)saveUser(user);// 2. 创建订单 (加入当前事务)orderService.createOrder(user.getOrder());// 如果上面任意一步失败,整个操作都会回滚}privatevoidsaveUser(Useruser){// ...}}@ServiceclassOrderService{@Transactional(propagation=Propagation.REQUIRED)// 可以省略,因为是默认值publicvoidcreateOrder(Orderorder){// ...}}🆕 REQUIRES_NEW (独立事务)
这个行为会挂起当前的事务(如果存在),并创建一个新的事务来执行该方法。
- 内层事务独立提交或回滚,不受外层事务影响。
- 外层事务被回滚,不会影响已经提交的内层事务。
- 但是,如果内层事务回滚并抛出了异常,而外层没有捕获处理,这个异常会继续向上抛出,导致外层事务也回滚。
适用场景:记录日志、发送通知等操作,即使主业务逻辑失败,这些操作也需要成功提交。
@ServicepublicclassUserService{@AutowiredprivateLogServicelogService;@TransactionalpublicvoidcreateUser(Useruser){// 1. 执行核心业务逻辑saveUser(user);try{// 2. 记录操作日志 (开启独立新事务)logService.logAction("CREATE_USER",user.getId());// 新事务提交}catch(Exceptione){// 捕获异常,防止日志失败影响主业务log.error("日志记录失败",e);}// 3. 模拟业务失败thrownewRuntimeException("业务失败!");// 主事务回滚,但日志已成功提交,不受影响}}@ServiceclassLogService{@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidlogAction(Stringaction,LonguserId){// ...}}📜 NESTED (嵌套事务)
NESTED是真正意义上的嵌套事务,它依赖于数据库的SAVEPOINT机制。
- 如果当前存在事务,则在该事务中设置一个保存点(Savepoint),并在该保存点内执行。
- 内层事务失败时,可以回滚到该保存点,而不影响外层事务的整体执行。
- 外层事务失败,会回滚整个事务,包括所有嵌套的子事务。
- 重要限制:它要求底层数据库(如 MySQL InnoDB)和 JDBC 驱动支持保存点。同时,在 Spring 的默认配置下,
REQUIRED和REQUIRES_NEW的使用频率远高于NESTED。
适用场景:需要精确控制部分回滚的复杂业务,例如一个订单中有多个商品,希望某个商品库存不足时仅回滚该商品的处理,但继续处理其他商品。
@ServicepublicclassOrderService{@AutowiredprivateItemServiceitemService;@TransactionalpublicvoidcreateOrder(Orderorder){for(Itemitem:order.getItems()){try{// 每个商品的处理作为一个嵌套事务itemService.processItem(item);}catch(Exceptione){// 捕获异常,仅回滚当前商品的处理,继续处理下一个log.warn("商品 {} 处理失败,已回滚",item.getId());}}}}@ServiceclassItemService{@Transactional(propagation=Propagation.NESTED)publicvoidprocessItem(Itemitem){// 减少库存等操作// 如果失败,仅回滚到保存点,不影响订单中其他商品的处理}}⚠️ 常见陷阱与解决方案
在处理事务嵌套时,有一个非常普遍的问题:同一个类中的方法直接调用会导致事务失效。
问题原因
Spring 的事务是基于 AOP 代理实现的。当一个类中的方法 A 直接调用同一个类中的方法 B(例如this.methodB())时,调用并没有经过 Spring 的代理对象,因此方法 B 上的@Transactional注解会被完全忽略。
解决方案
提取到独立的服务类 (推荐)
将需要事务控制的方法提取到一个独立的@Service类中,通过 Spring 注入调用。这是最清晰、最符合设计原则的方式。@ServicepublicclassServiceA{@AutowiredprivateServiceBserviceB;// 通过注入调用publicvoidmethodA(){// ...serviceB.methodB();// 事务生效}}@ServicepublicclassServiceB{@TransactionalpublicvoidmethodB(){// ...}}自我注入 (Self Injection)
通过将当前对象注入到自身,利用注入的引用进行调用,从而经过代理。@ServicepublicclassUserService{@AutowiredprivateUserServiceself;// 自我注入publicvoidmethodA(){// ...self.methodB();// 通过代理调用,事务生效}@TransactionalpublicvoidmethodB(){// ...}}通过 ApplicationContext 获取代理
注入ApplicationContext,从中获取当前 Bean 的代理对象。@ServicepublicclassUserService{@AutowiredprivateApplicationContextapplicationContext;publicvoidmethodA(){// ...UserServiceproxy=applicationContext.getBean(UserService.class);proxy.methodB();// 通过代理调用,事务生效}@TransactionalpublicvoidmethodB(){// ...}}