Spring的@Transactional注解踩坑

@Transactional介绍

Spring为开发人员提供了声明式事务的使用方式,即在方法上标记@Transactional注解来开启事务。大家在日常的开发中很多业务代码对数据进行操作的时候一定是希望有事务控制的。

比如电商卖东西业务,代码的逻辑是商家先生成一个订单(订单信息插入到数据库中),再将钱收入到自己的账户中(数据库中的money增加)。整个过程是要作为一个完整的事务来对待的,如果后面这个操作失败了,那么前者也一定不能插入成功,这时候就会用到事务的回滚。

@Transactional的常见错误使用姿势

抛出异常

常见错误一:异常没有传播出事务注解@Transactional标记的方法导致

经典的开发经验:

很多时候,在实际业务开发中,总希望接口能返回一个固定的类实例——这叫做统一返回结果。例如以Result类作为统一返回结果。

于是为了方便就直接在Service的方法中return一个Result类对象,为了避免受异常的影响而无法返回该结果集,就会使用try-catch语句,当业务代码出现错误而抛出异常时会捕获此异常,将异常信息写入Result的相关字段中,返回给调用者。

@Controller
@RestController
@Api( tags = "测试事务是否生效")
@RequestMapping("/test/transactionalTest")
@Slf4j
public class GoodsStockController {

    @Autowired
    private GoodsStockService goodsStockService;
    /**
     * create by: entropy
     * description: 事务无法回滚的方法
     * create time: 2022/1/25 21:38
     */
    @GetMapping("/exception/first")
    @ApiOperation(value = "关于异常的第一个方法,不能够回滚", notes = "因为异常未能被事务发现,所以没有回滚")
    @ResponseBody
    public Result firstFunctionAboutException(){
        try{
            return goodsStockService.firstFunctionAboutException();
        }catch (Exception e){
            return Result.server_error().Message("操作失败:"+e.getMessage());
        }
    }
}

其中的service中的方法

@Autowired
    private GoodsStockMapper goodsStockMapper;

    @Override
    @Transactional
    public Result firstFunctionAboutException() {
        try{
            log.info("减库存开始");
            goodsStockMapper.updateStock();
            if(1 == 1) throw new RuntimeException();
            return Result.ok();
        }catch (Exception e){
            log.info("减库存失败!" + e.getMessage());
            return Result.server_error().Message("减库存失败!" + e.getMessage());
        }
    }

最终测试结果事务没有回滚。我们都知道当程序执行时出现错误而抛出异常时,事务才会回滚,这里虽然出现了异常但却被方法本身消化了(catch掉了),异常没有被事务所发现,所以这样子是不会出现回滚的。因此正确的姿势是去掉service中的try-catch语句即可:

@Override
@Transactional
public void secondFunctionAboutException() {
    log.info("减库存开始");
    goodsStockMapper.updateStock();
    if(1 == 1) throw new RuntimeException();
}

通过这种处理方式可以实现事务的回滚。另外异常怎么办呢?很简单,将异常放在Controller层去处理就行。

总结:当标记了@Transactional注解的方法中出现异常时,如果该异常未传播到该方法外,则事务不会回滚;反之,只有异常传播到该方法之外,事务才会回滚。

常见错误二:异常突破@Transactional所标注的方法,事务依然没有回滚

@Override
@Transactional
public void thirdFunctionAboutException() throws Exception {
    log.info("减库存开始");
    goodsStockMapper.updateStock();
    if(1 == 1) throw new Exception();
}

眼尖的同学一眼就看到了问题:

Spring@Transactional注解就是默认只有当抛出RuntimeException运行时异常时,才会回滚。

Spring通常采用RuntimeException表示不可恢复的错误条件。也就是说对于其他异常,Spring觉得无所谓所以就不回滚。

那么对应的解法也有2种,请看:

解法一:手动catch捕获Exception,然后抛出runtimeException

@Override
@Transactional
public void thirdFunctionAboutException1(){
    try{
        log.info("减库存开始");
        goodsStockMapper.updateStock();
        if(1 == 1) throw new Exception();
    }catch (Exception e){
        log.info("出现异常"+e.getMessage());
        throw new RuntimeException("手动抛出RuntimeException");
    }
}

解法二:修改注解默认的@Transactional回滚的异常范围

@Override
@Transactional(rollbackFor = Exception.class)
public void thirdFunctionAboutException2() throws Exception {
    log.info("减库存开始");
    goodsStockMapper.updateStock();
    if(1 == 1) throw new Exception();
}

更高级的错误使用姿势

假设service业务类中有这样的2个方法:

@Override
public void privateFunctionCaller (){
    privateCallee();
}

@Transactional
private void privateCallee(){
    goodsStockMapper.updateStock();
    throw new RuntimeException();
}

serviceprivateFunctionCaller方法从而间接调用标注了@Transactional注解的方法privateCallee。执行代码后,发现事务并没有回滚。这是什么原因呢?

这就要提到@service注解的原理:spring是通过动态代理的方式来实现AOP的。也即AOP容器中的bean实际上都是代理对象。@Transactional注解的支持也正是通过AOP来实现的。Spring会对原对象中的方法进行封装(即检查到标有该注解的方法时,就会为它加上事务).。这个行为就叫做为目标方法进行增强。要想铜鼓增强的方式使得事务生效,那么方法必然不能是private的,实际上如果在ieda编辑器里在私有的方法上使用了@Transactional注解的话,编译器是会报错的。

实际上修改为public后事务还是没有回滚。这是为什么呢?

@Override
public void publicFunctionCaller (){
    publicCallee();
}

@Override
@Transactional
public void publicCallee(){
    goodsStockMapper.updateStock();
    throw new RuntimeException();
}

被注入的Service对象是代理对象,当调用publicCallee方法时,上面是没有@Transactional注解的。故只是简单执行service.function(),即在代理对象的方法publicFunctionCaller中,先由Service的原对象来调用自己的publicFunctionCaller方法,再由其调用自己的publicCallee方法。不会走代理对象增强过(带有事务)的publicCallee方法,事务也就不会回滚。

解决办法:显式的注入自己。缺点就是破坏了分层的结构,加大了代码耦合性

@Override
@Transactional
public void publicCallee(){
    goodsStockMapper.updateStock();
    throw new RuntimeException();
}

@Autowired
private GoodsStockService self;

@Override
public void aopSelfCaller (){
    self.publicCallee();
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值