超详细Spring注解@Transactional的注意事项

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档



1. @Transactional是什么?

@Transactional开启声明式事务的方法。声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

简而言之,@Transactional注解在代码执行出错的时候能够进行事务的回滚

2. @Transactional怎么用?(SpringBoot)

  • 在启动类上添加@EnableTransactionManagement注解。
    在这里插入图片描述

springboot 1.x使用事务需要在引导类上添加@EnableTransactionManagement注解开启事务支持
springboot 2.x可直接使用@Transactional玩事务,传播行为默认是REQUIRED

  • 用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
  • 在项目中,@Transactional(rollbackFor=Exception.class),如果类加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚
  • @Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。
    在这里插入图片描述

3. @Transactional的参数?

在这里插入图片描述

3.1 事务7种传播行为

引用文章:https://www.jb51.net/article/224832.htm#_label3_0_3_0
多事务方法之间相互调用,这个过程中事务是如何管理的问题(例如两个事务调用对方方法,事务该如何选择)

传播行为指的是,如果多个事务进行嵌套运行,子事务是否要和大事务共用一个事务
里面有多个选择:
在这里插入图片描述

  • REQUIRED 支持当前事务,如果不存在,就新建一个

  • SUPPORTS 支持当前事务,如果不存在,就不使用事务

  • MANDATORY 支持当前事务,如果不存在,抛出异常

  • REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务

  • NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务

  • NEVER 以非事务方式运行,如果有事务存在,抛出异常

  • NESTED 如果当前事务存在,则嵌套事务执行(嵌套式事务)

也有人这么解释,可以结合理解:
REQUIRED:如果有事务运行,当前方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行;

NOT_SUPPORTED:当前的方法不应该运行在事务中,如果有运行的事务,将它挂起。

REQUIRES_NEW:当前方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起。

MANDATORY:当前的方法必须运行在事务内部,如何没有正在运行的事务,就抛出异常。

SUPPORTS:如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中。

NEVER:前的方法不应该运行在事务中,如果有运行的事务,就抛出异常。

NESTED:如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行。

这七种事务传播机制最常用的就两种:

REQUIRED:一个事务,要么成功,要么失败

REQUIRES_NEW:两个不同事务,彼此之间没有关系。一个事务失败了不影响另一个事务(指REQUIRES_NEW不受REQUIRED影响,反过来不行)

伪代码练习
传播行为伪代码模拟:有a,b,c,d,e等5个方法,a中调用b,c,d,e方法的传播行为在小括号中标出

a(required){
    
    
	b(required);
	c(requires_new);
	d(required);
	e(requires_new);
	// a方法的业务
}

问题:

  • a方法的业务出现异常,会怎样?a,b,d回滚 c,e不回滚
  • d方法出现异常,会怎样?a,b,d回滚;c不回滚;e未执行
  • e方法出现异常,会怎样?a,b,d,e回滚 c不回滚,e方法出异常会上抛影响到上级方法
  • b方法出现异常,会怎样?a,b回滚 c,d,e未执行

加点难度:

a(required){
    
    
	b(required){
    
    
		f(requires_new);
		g(required)
	}
	c(requires_new){
    
    
		h(requires_new)
		i(required)
	}
	d(required);
	e(requires_new);
	// a方法的业务
}


问题:

  • a方法业务出异常?a,b,g,d回滚;f,c,h,i,e不回滚
  • e方法出异常?e,a,b,g,d回滚;f,c,h,i不回滚
  • d方法出异常?a,b,g,d回滚;f,c,h,i不回滚;e未执行
  • h,i方法分别出异常?h,i,c,a,b,g回滚;f不回滚;d,e未执行
  • i方法出异常?i,c,a,b,g回滚;f,h不回滚;d,e未执行
  • f,g方法分别出异常?f,g,b,a回滚;c,h,i,d,e未执行

前面这些都是在不catch异常的前提下
任何处崩,已经执行的requires_new不会崩
a(required){
b(required);//如过b设置了超时,超时不会生效的,因为a的required会传播给b
}
required:将之前事务用的connection传递给这个方法使用
required_new:这个方法直接使用新的connection

3.3.1关于嵌套事务的注意事项

案例:

有下面一接口SpuInfoService

public interface SpuInfoService extends IService<SpuInfoEntity> {
    
    

    PageVo queryPage(QueryCondition params);

    PageVo querySpuInfo(QueryCondition condition, Long catId);

    void saveSpuInfoVO(SpuInfoVO spuInfoVO);

    void saveSku(SpuVo spuVo, Long spuId);

    void saveBaseAttr(SpuVo spuVo, Long spuId);

    void saveSpuDesc(SpuVo spuVo, Long spuId);

    Long saveSpu(SpuVo spuVo);
}

它的实现类是SpuInfoServiceImpl
在这里插入图片描述

测试1:同一service + requires_new
添加事务:
在这里插入图片描述
这时,在保存商品的主方法中制造异常

在这里插入图片描述
bigSave的事务是REQUIREDsaveSpuREQUIREDsaveSpuDescREQUIRES_NEW

bigSave的事务嵌套saveSpusaveSpuDesc
也就是

     bigSave(required){
    
    
		saveSpu(requires_new);
		saveSpuDesc(required)
	}

预想的是如果bigSave的事务操作种出现异常,saveSpu会回滚,saveSpuDesc不会回滚

实际确实都会回滚,其实saveSpu在本例中加不加事务都一样,因为saveSpu加的是REQUIRED

REQUIRED如果有事务运行,当前方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行,因为外层是事务的,那么内层就如果出现异常一会回滚

关键在于 saveSpuDescREQUIRES_NEW,我们希望出现异常不让它回滚,因为Spring事务实际是基于AOP的,而AOP的底层是动态代理,也就是说Spring事务也是动态代理:通过代理对象开启事务,提交事务、回滚事务。

我们上图的调用方式本质是通过this调用的不是通过代理对象,这也就是为什么saveSpuDesc事务不生效的原因,所以事务要生效必须是代理对象在调用

解决方式1:把saveSpuDesc放在另外一个SpuDescService
把saveSpuDesc方法放到SpuDescService中

在这里插入图片描述

改造SpuServiceImpl中保存商品的方法,调用SpuDescServiceImpl的saveSpuDesc方法:在这里插入图片描述

spuDescService:
在这里插入图片描述
通过其他service对象(spuDescService)调用,这个service对象本质是动态代理对象

有事务的业务逻辑,容器中保存的是这个业务逻辑的代理对象

this:
在这里插入图片描述
解决方式2:创建本Service的代理对象
只需要把测试1中的this.方法名()替换成this代理对象.方法名()即可。

问题是怎么在service中获取当前类的代理对象?

在类中获取代理对象分三个步骤:

  • 导入aop的场景依赖:spring-boot-starter-aop
  • 开启AspectJ的自动代理,同时要暴露代理对象:@EnableAspectJAutoProxy(exposeProxy=true)
  • 获取代理对象:SpuInfoService proxy = (SpuInfoService) AopContext.currentProxy();
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

在这里插入图片描述

在这里插入图片描述

3.2 回滚策略

事务很重要的另一个特征是程序出异常时,会回滚。但并不是所有的异常都会回滚。

默认情况下的回滚策略:

  • 运行时异常:不受检异常,没有强制要求try-catch,都会回滚。例如:ArrayOutOfIndex,OutofMemory,NullPointException
  • 编译时异常:受检异常,必须处理,要么try-catch要么throws,都不回滚。例如:FileNotFoundException

可以通过@Transactional注解的下面几个属性改变回滚策略:
在这里插入图片描述

  • rollbackFor:指定的异常必须回滚
  • noRollbackFor:发生指定的异常不用回滚

3.2.1 测试编译时异常不回滚

在商品保存方法中制造一个编译时异常:
在这里插入图片描述
重启测试,注意pms_spu表中数据:

控制台报异常:
在这里插入图片描述
pms_spu表中的数据新增成功了
在这里插入图片描述
也就证明了编译时异常不回滚。

3.2.2 定制回滚策略

经过刚才的测试,我们知道:

ArithmeticException异常(int i = 1/0)会回滚FileNotFoundException异常(new FileInputStream(“xxxx”))不回滚

接下来我们来改变一下这个策略:
在这里插入图片描述
测试:
FileNotFoundException:在程序中添加newFileInputStream(“xxxx”),然后测试。

在这里插入图片描述

3.3 超时事务

@Transactional注解,还有一个属性是timeout超时时间,单位是秒。
在这里插入图片描述
timeout=3:是指第一个sql开始执行到最后一个sql结束执行之间的间隔时间。

即:超时时间(timeout)是指数据库超时,不是业务超时。

改造之前商品保存方法:SpuInfoServiceImpl类中
在这里插入图片描述
重启测试:控制台出现事务超时异常
在这里插入图片描述

3.4 只读事务

@Transactional注解一个属性是只读事务属性
在这里插入图片描述
如果一个方法标记为readOnly=true事务,则代表该方法只能查询,不能增删改。readOnly默认为false

给商品新增的事务标记为只读事务:
在这里插入图片描述
测试:
在这里插入图片描述

3.5 隔离级别

使用Isolation 参数可以设置事务的隔离级别

事务并发引起一些读的问题:

  • 脏读:一个事务可以读取另一个事务未提交的数据
  • 不可重复读: 一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配
  • 虚读(幻读: 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点

并发写:使用mysql默认的锁机制(独占锁)

解决读问题:设置事务隔离级别

  • read uncommitted(0)
  • read committed(2)
  • repeatable read(4)
  • Serializable(8)
    在这里插入图片描述

最后,有必要补充一下常用数据库的默认隔离级别:
MYSQL:默认为REPEATABLE_READ
SQLSERVER:默认为READ_COMMITTED
Oracle:默认隔离级别 READ_COMMITTED
注意:mysql数据库,当且仅当引擎是InnoDB,才支持事务(MyIsam引擎不支持事务)。

4. @Transactional 失效场景

引用文章:https://jiming.blog.csdn.net/article/details/110181822

1. @Transactional 应用在非 public 修饰的方法上

在这里插入图片描述
之所以会失效,是因为在 Spring AOP 代理时(如上图所示)TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource 的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息,方法如下:

在这里插入图片描述
此处我直接去了最关键得代码以展示,如图所见,此方法会检查目标方法的修饰符为 public不是 public 则不会获取 @Transactional 的属性配置信息。这就是原因所在!!

注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

2. @Transactional 注解属性 propagation 设置错误

这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。

  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行;
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起;
  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

3. @Transactional 注解属性 rollbackFor 设置错误

rollbackFor 可以指定能够触发事务回滚的异常类型。

Spring默认抛出了未检查(unchecked)异常(继承自 RuntimeException 的异常)或者 Error才回滚事务,其他异常不会触发回滚事务。

如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。
在这里插入图片描述
注意:若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚,Spring源码如下:
在这里插入图片描述

4. 在同一个类中方法调用,导致 @Transactional 失效

这也是经常犯错误的一个地方,要特别注意!!

开发中避免不了会对同一个类里面的方法调用,比如:有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。

那为啥会出现这种情况?

其实,这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码直接调用时,才会由Spring生成的代理对象来管理,进而由 TransactionInterceptor (事务拦截器)生成事务对象。

在这里插入图片描述

5. 异常被 catch 处理了,导致 @Transactional 失效

这种情况也是最常见的一种 @Transactional 注解失效场景,甚至很多人都不能准确定位到这个失效点。
在这里插入图片描述
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?

答案是:一定不能。而且会抛出异常:“org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only”

原因如下:

  • 当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。

  • spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出 RuntimeException。如果抛出 RuntimeException 并在你的业务方法中没有catch到的话,事务会回滚。

解决办法:
在业务方法中一般不需要catch异常,如果非要catch一定要抛出 throw new RuntimeException(),或者注解中指定抛异常类型 @Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据 commit 造成不一致。

所以,try…catch…也要活学活用,使用不当反倒会画蛇添足。

6. 数据库引擎不支持事务

这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。

常用的MySQL数据库默认使用支持事务的Innodb引擎。一旦数据库引擎切换成不支持事务的MyIsam,那事务就从根本上失效了。

5.总结

@Transactional 注解的看似简单易用,但如果对它的用法一知半解,还是会踩到很多坑的。

  • @Transactional 应用在非 public 修饰的方法上,不支持回滚;
  • @Transactional 注解属性 propagation 设置错误;
  • @Transactional 注解属性 rollbackFor 设置错误;
  • 在同一个类中方法调用,导致 @Transactional 失效;
  • 异常被 catch 处理了,导致 @Transactional 没办法回滚而失效;
  • 数据库引擎不支持事务

猜你喜欢

转载自blog.csdn.net/m0_45364328/article/details/125096817