You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

同类方法调用中事务仍生效?Spring Boot事务异常排查

Spring事务内部调用异常导致全量回滚问题分析

查阅了大量相关资料,均提到同类内部方法调用时子方法无事务。按此逻辑,执行下方代码后数据库应存在method2、无method1,但实际两者均未保存。环境:Spring Boot 3.3.1,Transactional默认代理模式。

问题代码

@Service
public class MyService {
    @Transactional
    public void add(AddInventoryChangeDto dto) {
        InventoryChange c = new InventoryChange();
        c.setName("method1");
        c.setCreateUserId("95a9d193354d19b833408d1c486f79c3");
        inventoryChangeMapper.insert(c);
        inStock();
    }

    private void inStock() {
        InventoryChange c = new InventoryChange();
        c.setName("method2");
        c.setCreateUserId("95a9d193354d19b833408d1c486f79c3");
        inventoryChangeMapper.insert(c);
        throw new RuntimeException("hh");
    }
}

@RestController
public class MyController {
  @Autowired
  private MyService service;

  @PostMapping("/test")
  public void test(@RequestBody AddInventoryChangeDto dto) {
    service.add(dto);
  }
}

原因分析

  • 核心误区:混淆了「子方法无独立事务」和「子方法脱离父方法事务上下文」的概念。
  • add方法标记了@Transactional,整个方法处于一个Spring管理的事务中。内部调用的inStock是private方法,Spring动态代理无法拦截该调用,因此inStock确实没有自己的独立事务,但它完全运行在add方法的事务上下文内。
  • RuntimeException是Spring默认触发事务回滚的异常类型,当inStock抛出该异常时,Spring会将整个add方法的事务标记为回滚状态,最终两个insert操作都会被回滚,数据库中不会留存任何记录。

实现预期效果的方案

如果需要让inStock的操作不受add方法事务回滚影响,即保存method2、回滚method1,可以采用以下两种方式:

方案1:抽离子方法到独立Service

将inStock方法移至另一个Spring管理的Service类,给该方法标记@Transactional(propagation = Propagation.REQUIRES_NEW)以强制开启新事务:

@Service
public class StockService {
    @Autowired
    private InventoryChangeMapper inventoryChangeMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void inStock() {
        InventoryChange c = new InventoryChange();
        c.setName("method2");
        c.setCreateUserId("95a9d193354d19b833408d1c486f79c3");
        inventoryChangeMapper.insert(c);
        throw new RuntimeException("hh");
    }
}

@Service
public class MyService {
    @Autowired
    private StockService stockService;

    @Transactional
    public void add(AddInventoryChangeDto dto) {
        InventoryChange c = new InventoryChange();
        c.setName("method1");
        c.setCreateUserId("95a9d193354d19b833408d1c486f79c3");
        inventoryChangeMapper.insert(c);
        stockService.inStock();
    }
}

方案2:通过自身代理对象调用

在当前Service中注入ApplicationContext,获取自身的代理对象,将inStock改为public方法并标记@Transactional(propagation = Propagation.REQUIRES_NEW):

@Service
public class MyService {
    @Autowired
    private ApplicationContext context;
    @Autowired
    private InventoryChangeMapper inventoryChangeMapper;

    @Transactional
    public void add(AddInventoryChangeDto dto) {
        InventoryChange c = new InventoryChange();
        c.setName("method1");
        c.setCreateUserId("95a9d193354d19b833408d1c486f79c3");
        inventoryChangeMapper.insert(c);
        // 通过代理对象调用inStock,触发新事务
        context.getBean(MyService.class).inStock();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void inStock() {
        InventoryChange c = new InventoryChange();
        c.setName("method2");
        c.setCreateUserId("95a9d193354d19b833408d1c486f79c3");
        inventoryChangeMapper.insert(c);
        throw new RuntimeException("hh");
    }
}

以上两种方案中,inStock的新事务会独立提交,而add的事务会因异常回滚,最终数据库中只会存在method2的记录,符合最初的预期。

内容的提问来源于stack exchange,提问作者user1434702

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 18:40:13