当前位置: 首页 > 后端技术 > Java

【Spring声明式事务用过的那些坑】@Transactional注解的坑最全攻略

时间:2023-04-01 18:43:05 Java

1.前言朋友们大家好,我是啤酒熊,今天想和大家聊聊Spring坑中的一些数据库事务.Spring为开发者提供了一种使用声明式事务的方式,即在方法上标记@Transactional注解来启动事务。我们都知道,业务代码在对数据进行操作的时候,一定要有事务控制。比如写一个商家卖东西的业务代码,代码的逻辑是商家先生成订单(订单信息插入数据库),然后收款到他的账户(里面的钱)数据库增加)。如果后面的操作失败了,那么前者肯定也没有插入成功,这时候就会用到事务回滚。虽然大部分做后端开发的同学都有这方面的概念,但是在使用@Transactional注解的时候还是会出现一些错误。前几天在给公司新同学写的代码做codereview的时候,看到他们的Spring项目中@Transactional注解的一些错误使用。在纠正他们的错误的同时,我不禁想到我也掉进了这些坑ヽ(ー_ー)ノ所以我想做一个指南来避免使用这个注解的陷阱~并与社区分享.本文将介绍@Transactional在普通业务开发中常见的几种错误用法,并给出相应的错误代码示例。针对每种错误类型,说明其原因,并给出使用@Transactional注解的正确使用姿势。接下来,让我们一起来看看吧!2.实验准备2.1数据库我们在数据库中定义了一张goods_stock商品库存表,并赋值一些初始数据:表示当前商品id为good_0001的商品库存为10件。2.2SpringBoot+Mybatis我们在Java方法中使用Mybatis来减少库存,在方法上标注@Transactional注解,看注解是否可以使事务失效,遇到错误回滚。项目结构如下:我们将在Controller层使用Swagger调用接口,在接口中调用Service层GoodsStockServiceImp中的具体业务代码,即减库存操作。具体sql语句执行库存减10操作,即业务代码执行成功,商品库存变为0:updategoods_stocksetstock=stock-103.抛出异常3.1异常传播不显示@Transactional标记的方法很多时候,在实际业务开发中,总是希望一个接口能够返回一个固定的类实例——这叫做统一返回结果。本文使用Result类作为统一返回结果。详情见本文附带的代码。因此,为了方便,可以直接在Service方法中返回一个Result类对象。为了避免被异常影响而无法返回结果集,会使用try-catch语句。当业务代码出现错误,抛出异常时,该异常会被捕获,异常信息会被写入Result的相关字段,返回给调用方。下面给出了这种类型的一个例子:控制器层:@Controller@RestController@Api(tags="测试交易是否有效")@RequestMapping("/test/transactionalTest")@Slf4jpublicclassGoodsStockController{@AutowiredprivateGoodsStockServicegoodsStockService;/***创建者:啤酒熊*描述:第一种方法。*创建时间:2021/7/2521:38*/@GetMapping("/exception/first")@ApiOperation(value="第一个关于异常的方法不能回滚",notes="因为异常不是可以被事务找到,所以没有回滚")@ResponseBodypublicResultfirstFunctionAboutException(){try{returngoodsStockService.firstFunctionAboutException();}catch(Exceptione){returnResult.server_error().Message("操作失败:"+e.getMessage());服务中的方法:@AutowiredprivateGoodsStockMappergoodsStockMapper;@Override@TransactionalpublicResultfirstFunctionAboutException(){try{log.info("开始减少库存");goodsStockMapper.updateStock();如果(1==1)抛出新的RuntimeException();返回结果.ok();}catch(Exceptione){log.info("库存减少失败!"+e.getMessage());returnResult.server_error().Message("减库存失败!"+e.getMessage());在firstFunctionAboutException方法的try代码块中,会抛出RuntimeException正常,但是可以回滚吗?我们不妨通过实验看一下:使用Swagger调用接口:调用接口后,按理说应该回滚交易,库存数量不会变成0,但是结果是:为了节省篇幅,下面这些截图不再出现,而是用文字代替。显然事务没有被回滚。我们都知道,当程序执行过程中出现错误,抛出异常时,事务就会回滚。这里虽然有异常,但是被方法自己消化了(catchit),异常没有被事务发现,所以这种方式不会回滚。下面我们给出相关的正确解决方案——去掉服务中的try-catch语句:@Override@TransactionalpublicvoidsecondFunctionAboutException(){log.info("开始减少库存");goodsStockMapper.updateStock();if(1==1)thrownewRuntimeException();}这样就可以实现事务回滚。(但是,这种情况下,异常怎么办?不能直接报异常,很简单,把异常放到Controller层处理就行了)这里总结一下第一个坑避坑指南:当标记@Transactional注解的方法中发生异常时,如果异常没有传播到方法外,则事务不会回滚;否则,仅当异常传播到方法外部时,事务才会回滚。3.2明明抛出异常却不回滚现在我们都知道,当程序执行过程中出现错误并抛出异常时,只要不去处理异常,让异常突破@Transactional标记的方法即可,您可以实现所需的回滚。但事实真的如此吗?再来看另外一种情况:@Override@TransactionalpublicvoidthirdFunctionAboutException()throwsException{log.info("开始减少库存");goodsStockMapper.updateStock();if(1==1)thrownewException();}Actual上面,这个方法中的事务是不会回滚的。这也是我们在实际开发中最常犯的错误。我们以为只有抛出异常才会回滚,结果却被现实打了耳光。但是我觉得这并不是什么丢人的事情,因为我们在使用一个工具的时候,一开始可能没有精力和能力去学习它的一些原理,从而掉进一些不容易发现的坑里.只要后面你不断学习,这些坑你就会慢慢填上,你就会越来越强。好了,言归正传,这个事务为什么不回滚。我们把这个方法和上面的secondFunctionAboutException对比了一下,发现只是RuntimeException和Exception的区别。确实是这样,因为Spring的@Transactional注解默认只有抛出RuntimeException运行时异常才会回滚。Spring一般采用RuntimeException来表示不可恢复的错误情况。也就是说对于其他的异常,Spring不关心所以不回滚。下面我给出两种解决方案:@Override@TransactionalpublicvoidthirdFunctionAboutException1(){try{log.info("开始减少库存");goodsStockMapper.updateStock();如果(1==1)抛出新的异常();}catch(Exceptione){log.info("发生异常"+e.getMessage());thrownewRuntimeException("手动抛出一个RuntimeException");}}@Override@Transactional(rollbackFor=Exception.class)publicvoidthirdFunctionAboutException2()throwsException{log.info("开始减少库存");goodsStockMapper.updateStock();if(1==1)thrownewException();}第一个我们手动抛出一个RuntimeException,第二个是改变@Transactional回滚的默认异常设置(RuntimeException继承Exception异常)。@Transactional(rollbackFor=Exception.class)这里总结了回避指南的第二个坑:默认情况下,如果我们抛出的异常不是RuntimeException,事务仍然不会回滚;您需要手动抛出RuntimeException或更改Spring中的@Transactional默认配置。四、事务仍然没有生效即使我们注意到了exception和@Transactional的关系,正确的避开了这些坑,我们还是会掉进一些不容易发现和理解的坑中。本节我们将继续举出反例,说明这些例子中交易不生效的原因,并给出解决方案。在本节中,您还将了解@Transactional如何处理SpringAOP。4.1示例1在服务中添加这两个方法:@OverridepublicvoidprivateFunctionCaller(){privateCallee();}@TransactionalprivatevoidprivateCallee(){goodsStockMapper.updateStock();thrownewRuntimeException();}在Controller中调用服务的privateFunctionCaller方法间接调用了@Transactional注解标注的方法privateCallee。执行完代码,发现事务没有回滚。这就是为什么?我们在Service类上打上@Service注解,表示该类作为Bean注入到AOP容器中,Spring通过动态代理实现AOP。也就是说,AOP容器中的bean实际上是代理对象。Spring也以这种方式支持@Transactional。Spring会把方法封装在原来的对象中(即当它检查这个注解标记的方法时,会为其添加一个事务)。这种行为被称为目标方法增强。虽然Spring实现动态代理的方式是CGLIB,但是我想用JDK动态代理的实现来说明一下,因为比较容易理解。从service.function()可以看出,如果使用了proxy的增强方式,函数一定不能是private的。所以私有方法上的事务无法生效,自然也就无法回滚。其实,当你写上面的代码时,如果你使用的编译器是IDEA,编译器会提示并报错,当然也只是报红,并不影响编译和执行。在Java中实现动态代理的方式有JDK和CGLIB的实现。不了解动态代理的同学可以了解代理模式和MaBtais在Spring中的实现。4.2示例2那么我们只需要将private更改为public?下面这段代码是很多同学第一次使用@Transactional时经常掉进去的坑。@OverridepublicvoidpublicFunctionCaller(){publicCallee();}@Override@TransactionalpublicvoidpublicCallee(){goodsStockMapper.updateStock();thrownewRuntimeException();}当我们在Controller中调用Service中的publicFunctionCaller时,发现事务仍然无法返回出去,这是为什么呢?上面我们提到,在Controller中,注入的Service对象其实就是它的代理对象。当调用publicCallee方法时,上面没有@Transactional注解。因此,它只是简单地执行service.function(),即在代理对象的方法publicFunctionCaller中,Service的原对象调用自己的publicFunctionCaller方法,然后再调用自己的publicCallee方法。代理对象(有事务)增强的publicCallee方法根本不会被调用。自然事务不会回滚。解决方法,我想大家可以自己找找,就是在Controller中注入service的bean直接调用@Transactional标记的方法,比如调用上一篇文章中的secondFunctionAboutException。当然,我们也可以曲线救国,将自己注入到服务中,这样我们就可以实现代理对象调用增强方法:@Override@TransactionalpublicvoidpublicCallee(){thrownewRuntimeException();}@AutowiredprivateGoodsStockServiceself;@OverridepublicvoidaopSelfCaller(){self.publicCallee();}但是显然这样不符合层级结构,不够优雅。这里总结一下避坑指南第三个坑:@Transactioal注解标注的方法必须是public的,必须被注入的bean直接调用才能回滚事务。至此,@Transactional的避坑攻略就结束了。如果您有任何问题,请发表评论,让我们互相交流。也希望大家多多喜欢,以后会继续输出更多优质的文章!本文所有代码都放在Gitee上,有需要的小伙伴可以自行获取。