@Transactional
注解的失效场景最初学习Spring时,B站的杨老师就说过“在工作中不要过于依赖
@Transactional
注解实现事务,我们不仅要掌握注解实现事务,还需要掌握通过配置文件实现事务”,当时他没有明确说为什么,现在我应该是大致了解了,因为@Transactional
注解对于新手而言是存在很多坑的,在很多情况下@Transactional
注解都会失效,现在就让我们来详细学习哪些情况下@Transactional
注解实现的事务会失效吧。PS:关于如何通过配置文件实现事务请参考Spring学习笔记
事务:指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。可以理解事务就是一段代码块或者一行SQL,这段代码或这行SQL会更新数据库,事务具有基本的ACID特性,以此保障数据的安全性。
事务管理:由事务管理器1、恢复管理器2、锁管理器3、死锁管理器4、缓存管理器5构成
事务管理的作用:管理事务相关的资源;更容易处理复杂的事务;简化事务相关的操作,让程序员更关注业务
Spring中提供了两种事务管理机制:
AOP
面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低。而声明式事务有两种方式实现,方式一是基于@Transaction
注解实现,方式二是基于XML
实现。(Spring事务管理的亮点在于声明式事务管理,它允许我们通过声明的方式,在IoC配置中指定事务的边界和事务属性,Spring会自动在指定的事务边界上应用事务属性。相对于编程式事务来说,这种方式十分的方便,只需要在需要做事务管理的方法上,增加@Transactional注解,以声明事务特征即可)大多数 Spring 框架的用户选择声明式事务管理,因为它对应用代码的影响最小, 因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务 管理,虽然比编程式事务管理(这种方式允许你通过代码控制事务)少了一点灵 活性。
@Transactional:可以作用在接口、类、方法
Interface
上并且配置了Spring AOP 使用CGLib
动态代理,将会导致@Transactional
注解失效@Transactional
注解放在类上时,代表这个类所有公共(public)非静态(static)的方法都将启用事务功能,且都会被 Spring 的事务管理器进行管理@Transactional
配置在方法上,该方法被当成一个独立的事务,且被事务管理器管理。当类配置了@Transactional
,方法也配置了@Transactional
,此时方法的事务会覆盖类的事务配置信息@Transactional的属性
propagation
属性
propagation
代表事务的传播行为,默认值为 Propagation.REQUIRED
,其他的属性信息如下:
Propagation.REQUIRED
:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务
Propagation.SUPPORTS
:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行
Propagation.MANDATORY
:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常
Propagation.REQUIRES_NEW
:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED
模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW
模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW
会暂停 a方法的事务 )
Propagation.NOT_SUPPORTED
:以非事务的方式运行,如果当前存在事务,暂停当前的事务
Propagation.NEVER
:以非事务的方式运行,如果当前存在事务,则抛出异常
Propagation.NESTED
:和 Propagation.REQUIRED
效果一样
isolation
属性
事务的隔离级别,默认值为 Isolation.DEFAULT
Isolation.DEFAULT
:使用底层数据库默认的隔离级别。
Isolation.READ_UNCOMMITTED
:读取未提交数据(会出现脏读, 不可重复读) 基本不使用
Isolation.READ_COMMITTED
:读取已提交数据(会出现不可重复读和幻读)
Isolation.REPEATABLE_READ
:可重复读(会出现幻读)
Isolation.SERIALIZABLE
:串行化
timeout
属性:事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务
readOnly
属性:指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 readonly
为 true
rollbackFor
属性:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型
noRollbackFor
属性:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型
@Transactional
应用在非 public 修饰的方法上
@Transactional
注解修饰的方法必须是public
修饰的,同样的@Transactional
修饰类时,也只有类中使用pulbic
修饰的方法才能成为事务。须知:使用@Transactional修饰的方法,必须是public修饰、非static修饰、非final修饰的,一个不满足就会导致事务失效
原因:
由于Spring的事务是通过AOP实现的,在AOP代理时,事务拦截器TransactionInterceptor
在目标方法执行前后进行拦截,DynamicAdvisedInterceptor
(CGlib动态代理时的事务拦截器)的 intercept
方法或 JdkDynamicAopProxy
(JDK动态代理时的代理对象)的 invoke
方法都会间接调用 AbstractFallbackTransactionAttributeSource
的 computeTransactionAttribute
方法,获取Transactional
注解的事务配置信息,如下图所示:
备注:在老版本的Spring中,这个需要十分注意,因为当我们在非pulic
方法上加@Transactional
,它在编译阶段是没有任何报错信息的,但是新版的Spring是能够在编译阶段就能够进行报错,所以只要是使用较新版本的版本,基本没可能会犯这种错误,比如我当前使用的Spring版本是5.2.15,直接就在编译阶段报错了:
propagation
属性设置错误当我们将
propagation
属性的值设置为一下几种取值就会导致事务失效:
Propagation.NOT_SUPPORTED
:以非事务的方式运行,如果当前存在事务,暂停当前的事务
Propagation.NEVER
:以非事务的方式运行,如果当前存在事务,则抛出异常
rollbackFor
属性设置错误Spring默认抛出了未检查
unchecked
异常(继承自RuntimeException
的异常)或者Error
才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor
属性;若在目标方法中抛出的异常是rollbackFor
指定的异常的子类,事务同样会回滚。
[
Spring底层通过getDepth
方法来判断出现异常是否需要进行事务回滚
@Transactional
失效同一个类中,A方法是非事务性方法,但是B方法是事务性方法,此时A调用B就会导致B的事务失效。
原因:
这个和场景一的原因是类似的,事务的实现是基于AOP的,而AOP的实现又是基于动态代理的,而动态代理的本质就算对方法的增强,如果想要使用增强的方法(也就是想要使用事务方法),就必须是通过代理对象去触发目标对象的方法,这个我相信只要学过设计模式都是能够理解的。
解决方案:
通过
AopContext.currentProxy()
这个API获取当前类的代理对象
示例:
@Service
Class ServiceImpl implements IService{
// 普通方法
@Override
public Result A() {
......
IService proxy = (Iservice) AopContext.currentProxy();
return proxy.B();
}
// 事务方法
@Override
@Transactional
public Result B() {
......
return Result.ok();
}
}
推荐阅读:每日一个设计模式之【代理模式】
@Transactional
失效当一个事务方法中抛出了异常,此时该异常通过
try...catch
进行了捕获,此时就会导致该方法的事务注解@Transactional
失效
示例:
@Resource
private IBService bService;
@Service
Class AServiceImpl implements IAService{
@Transactional
public Result A(Student s) {
try {
bService.save(s);
} catch (Exception e) {
e.printStackTrace();
}
return Result.ok();
}
}
此时会报错org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
原因:因为bService执行save方法过程中出现了异常,所以bService告诉事务管理器,当前事务需要被rollback
,但是aService中使用try...catch
捕获了异常,它认为当前事务并没有发生异常,程序是处于正常状态,于是aService就告诉事务管理器,当前事务需要被commit
,结果事务管理器发现收到两个矛盾的信号,它也搞不清是该rollback还是该commit,于是就抛了个UnexpectedRollbackException
异常。
也就是说Spring中,事务是在方法调用时开始的,业务方法执行完毕后才执行rollback或commit操作,事务是否被回滚取决于是否抛出异常,且该异常是否满足场景三(也就是说抛出的异常是否有被rollbackFor
指定,或rollbackFor
指定异常的子类)。如果一定要使用try..catch
时,一定要抛出异常(且抛出的异常必须满足场景三,一般直接抛一个运行时异常就可以了 throw new RuntimeException()
,运行时异常是rollbackFor
默认指定的异常),而不只是打印异常信息。
综上所诉:在Service层中,方法中最好不要随便写try...catch
,如果写了则一定要手动抛异常
Spring的事务本质还是得靠数据库引擎的支持,如果数据库引擎不支持事务,那么Spring就算使用事务也是白搭。常用的MySQL数据库默认使用支持事务的
innodb
引擎。一旦数据库引擎切换成不支持事务的myisam
,那事务就从根本上失效了。当然相信这个问题出现的概率很小,但并不代码没有,我们还是需要有一定了解的
注意:从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM。也就是说是从MySQL5.5.5开始,MySQL才支持事务
想要
@Transactional
注解实现声明式事务,首先就需要开启事务,开启事务就三步:
- 配置事务管理器
- 开启事务的注解驱动
- 使用
@Transactional
PS:这个事件对于向我这种初学者来说概率还是存在的,对于老手应该不太可能会出现这种低级错误了
开启事务相关配置:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourceRef">property>
bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
备注:在SpringBoot中,只需要在启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)
这个注解,就能使用基于注解实现的声明式事务了,等价于上面的配置
Spring的事务管理核心是动态代理,不是动态代理的Bean是无法进行被Spring进行事务管理的
将Bean纳入Spring容器管理的方法:
方式一:配置XML
<bean id="xxx" class="xxx.xxx.xxx">bean>
方式二:添加注解,比如:@Controller、@Service、@Repository、@Conponent……
spring 的事务是通过LocalThread来保证线程安全的,事务和当前线程绑定,此时开启新线程执行业务,这个新线程的业务就会事务失效,因为事务是基于动态代理的,要想有事务,需要被动态代理。这里提供一种解决方法,可以将新的业务单独封装成一个方法,然后改方法上添加一个@Transactional,或者将这个无法单独抽取到一个类中,将该类交给IOC容器进行管理,这样就能让新线程的业务具有事务了
@Transactional
使用事务具有几个基本的要求:
总而言之,使用@Transactional来启动事务,有很多坑,对于简单的业务还是推荐直接使用注解进行事务管理,对于复杂的业务还是推荐使用XML进行事务管理
参考文章:
- 一口气说出 6种,@Transactional注解的失效场景 - 掘金 (juejin.cn)
- 咱们从头到尾说一次 Spring 事务管理(器) - 掘金 (juejin.cn)
- spring 事务失效的 12 种场景-CSDN博客
在此致谢(^^ゞ
事务管理器:负责产生事务并为其分配事务标识 ↩︎
恢复管理器:子事务提交时,负责将子事务的日志链接到父事务的日志上,确保事务的一致性原则 ↩︎
锁管理器:事务申请锁时,负责判断锁的相容性 ↩︎
死锁管理器:负责检测死锁 ↩︎
缓存管理器:提供对事务标识的缓存 ↩︎