@Transactional失效的场景都有哪些呢?本章节针对@Transactional的问题,做以下总结整理。
-
1、同一个类中,方法内部调用事务失效
-
2、事务方法被final、static修饰
-
3、当前类没有被Spring管理
-
4、非public修饰的方法(存在版本差异)
-
5、事务多线程调用
-
6、数据库本身不支持事务
-
7、异常被方法内部try catch捕获,没有重新抛出
-
8、嵌套事务回滚多了
-
9、rollbackFor属性设置错误
-
10、设置不支持事务的传播机制
以上我们列举了10种场景,接下来我们针对不同的场景来具体的分析下。
SpringBoot版本:3.1.3
SpringFramework版本:6.0.11
场景分析
一、代理不生效导致
1、同一个类中,方法内部调用事务失效
场景一:同一个类中,addOrder()方法无事务,addOrder2()方法存在事务,addOrder()调用addOrder2()。
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
public int addOrder(Double amount, String address) {
int order = addOrder2(amount, address);
return order;
}
@Transactional
public int addOrder2(Double amount, String address) {
int order = orderMapper.addOrder(amount, address);
int i = order / 0;
return order;
}
}
我们通过外部方法调用addOrder()方法,来完成数据库的插入,通过手动的设置异常order/0,来观察addOrder2()方法中的数据是否会正常回滚。
通过数据库的结果显示,数据正常入库了,证明了我们的事务并未生效。
场景二:同一个类中,addOrder()和addOrder2()都存在事务,addOrder()调用addOrder2()。
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Transactional
public int addOrder(Double amount, String address) {
int order = addOrder2(amount, address);
return order;
}
@Transactional
public int addOrder2(Double amount, String address) {
int order = orderMapper.addOrder(amount, address);
int i = order / 0;
return order;
}
}
order/0 产生异常之后,通过数据库的结果显示,发现数据并未入库,说明事务生效了。
通过两种场景对比,我们发现并不是所有同一个类,方法的内部调用事务都会失效。
那接下来,我们就具体聊聊场景一:事务不生效的原因。
外部代码调用addOrder()方法时,并没有直接进入目标方法,而是首先进入了DynamicAdvisedInterceptor的intercept()方法中。
这是因为我们的orderService的bean在项目在项目启动的时候就生成一个代理对象,这里调用addOrder()方法的,其实是代理对象,而代理对象对目标方法的调用,会转发进入CGLIB动态代理类的intercept()方法中进行增强。
我们来看下DynamicAdvisedInterceptor的intercept()逻辑。
通过本地debug 我们可以看到如果addOrder()方法未加@Transactional的情况下,会直接进入45行的AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse)逻辑中。
而该方法,则直接通过mehod.invoke()完成了对目标方法的调用,没有做额外的逻辑处理。
而在addOrder()方法内部调用addOrder2()方法的时候,也是对目标方法的直接调用。好像addOrder2()方法上@Transactional注解的存在没有起什么作用。
当addOrder2()方法发生异常之后,也就没有什么机制来处理事务的回滚。
那如果我们把controller中的调用换成orderService.addOrder2(),在addOrder2()方法上打断点,看下代码的处理逻辑。
在方法调用栈中,我们发现了TransactionInterceptor事务拦截器。
而通过TransactionInterceptor 这个机制,我们完成了对目标方法的增强,例如事务控制,主要处理逻辑在TransactionAspectSupport.invokeWithinTransation()中。
主要内容如下:
-
createTransactionIfNecessary() : 创建一个标准事务
-
invocation.proceedWithInvocation:调用拦截器链条中的下一个拦截器,最终目标方法会被调用
-
completeTransationAfterThrowing():调用出现异常 进行处理
-
cleanupTransactionInfo:清除事务信息
-
commitTransactionAfterRetruning():提交事务commit
当addOrder2()异常发生时,TransactionInterceptor会对事务做异常处理。
通过以上的内容分析,我们很清晰的看出来,只有当调用addOrder2()方法是通过代理类进行的时候,才能触发事务的拦截。
2、事务方法被final、static修饰
示例代码:
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Transactional
public final int addOrder(Double amount, String address) {
int order = orderMapper.addOrder(amount, address);
int i = order / 0;
return order;
}
@Transactional
public int addOrder2(Double amount, String address) {
int order = orderMapper.addOrder(amount, address);
int i = order / 0;
return order;
}
}
失效原因:CGLIB是通过生成目标类子类的方式生成代理类的,被final、static修饰的方法,无法被子类重写。
当通过外部接口调用addOrder()方法时,我们代理类不是走DynamicAdvisedInterceptor拦截器,而是直接调用了目标方法。
为什么会出现这个现象呢?
在SpringBoot启动,我们创建bean的过程中,会给orderServiceImpl类生成一个代理类,在创建代理类的过程中,在ProxyCallbackFilter.accept()方法中,会指定目标方法执行的时候,触发的拦截器数组中的顺序。
例如addOrder2()方法,filter.accept()方法返回的是0。
在orderServiceImp代理类中0 的位置,则就是DynamicAdvisedInterceptor拦截器。
但是addOrder()方法,却没有出现在Enhancer.emitMethods()的方法入参中actualMethods。也就并未执行filter.accept()逻辑,获取代理类中拦截器的位置。
actualMethods为什么未出现final、static修饰的方法呢?
我们继续顺着源码,往上着,发现在Enhancer.getMethods获取方法的逻辑中,源码是过滤掉了static、和final修饰的方法。
-
1、如果rejectMask为8,表示修饰符中是否包含static;如果结果==0为true,则表示不包含,反之false则包含,则从集合methods中剔除。
-
2、如果rejectMask为16,表示修饰符中是否包含final;如果结果==0为true,则表示不包含,反之false则包含,则从集合methods中剔除。
这里补充一个知识点:
Java中修饰符的数值大小
public:1
private:2
protected:4
static:8
final:16
那是不是表示,我们的代理类中是没有也final、static修饰的方法?
通过本地生成的动态代理类,我们也验证了这一说法,我们新生成代理类orderServiceImpl中,确实没有被final修饰的方法addOrder(),只有addOrder2()。
知识扩展:如何本地生成动态代理类?(用于springBoot中的bean动态代理类的生成)
1、定义一个工具类,用于文件输出
public class ProxyFileOutUtils {
public static void saveProxyFile(String className, byte[] bytes){
if (className.equals("com.example.service.OrderServiceImpl$$SpringCGLIB$$0")){
String fileName = className + ".class";
File file = new File("src/main/java/com/example/out", fileName);
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(bytes);
System.out.println("Proxy class written to: " + fileName);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、在类AbstractClassGenerator.generate()里边,打上断点,
在断点里边定义自定义处理逻辑:ProxyFileOutUtils.saveProxyFile(className, b)
springBoot启动的时候,就会生成代理类,我们这里主要是获取到代理类的名称calssName,和代理类的字节数组b,来本地生成代理类。
3、当前类没有被Spring管理
例如:Service类中没有@Service注解
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Transactional
public int addOrder(Double amount, String address) {
int order = orderMapper.addOrder(amount, address);
int i = order / 0;
return order;
}
}
从以上的两种场景分析,我们得知@Transactional事务生效的前提条件是需要代理类对目标方法的调用,才能触发事务处理,而代理类是在springBoot启动时创建bean的时候,处理的。如果我们的类没有@Service注解,就不会交给spring容器初始化处理,也就无法为目标类生成代理类。
二、框架或者底层不支持
4、非public修饰的方法(存在版本差异)
示例代码:
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Transactional
protected int addOrder(Double amount, String address) {
int order = orderMapper.addOrder(amount, address);
int i = order / 0;
return order;
}
}
通过外部接口的调用,异常之后,发现事务正常回滚,数据库数据并没有入库,说明事务是生效的。
springframework的版本:6.0.11
不是说非public修饰的方法,事务不生效吗?
通过本地debug分析源码得知,在spring版本6.0.11中是支持proected修饰的方法的。
在创建事务属性AbstractFallbackTransactionAttributeSource.computeTransactionAttribute()的逻辑中,allowPublicMethodOnly()方法的返回值,默认是false,即使在第二个条件中,方法修饰符不是public,也不会返回null,事务属性TransactionAttribute正常被创建。
而allowPublicMethodOnly()的值publicMethodOnly是在transactionAttributeSource的bean初始化的时候赋值的,默认是false。
对比了spring5.3.25的版本,发现之前的版本,publicMethodOnly默认值确实为true。
如果publicMethodOnly为true的情况下,AbstractFallbackTransactionAttributeSource.computeTransactionAttribute()方法中,如果第二个条件,方法的修饰符非pulic,if中条件就会成立,则事务属性返回null,就会导致@Transactional事务失效。
5、事务多线程调用
示例代码:
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Autowired
private SysUserServiceImpl sysUserService;
@Transactional(rollbackFor = Exception.class)
public int addOrder(Double amount, String address){
int order = orderMapper.addOrder(amount, address);
new Thread(() -> {
sysUserService.saveUser();
}).start();
return order;
}
}
@Service
public class SysUserServiceImpl {
@Transactional
public void saveUser(){
throw new RuntimeException("新增人员失败");
}
}
当我们使用外部接口调用addOrder()方法时,sysUserService.saveUser()方法发生异常。但是发现orderMapper.addOrder()的数据正常入库了,事务失效。
通过本地debug和分析源码得知,我们的事务是给线程绑定的(TransactionAspectSupport.prepareTransactionInfo())。
在执行sysUserService.saveUser()目标方法的时候,我们通过代理类执行逻辑,获取到的事务AbstractPlatformTransactionManager.getTransaction()其实是重新创建的一个事务。
因此当saveUser()方法发生异常时,addOrder()方法的事务未能同步回滚数据。
6、数据库本身不支持事务
Spring事务的底层,还是依赖于数据库本身的事务支持。在MySQL中,Myisam存储引擎是不支持事务的,InnoDB引擎才支持事务。
这种问题出现的概率很小,在Mysql5之后,默认情况下是使用的InnoDB引擎存储
如果是历史项目,发现事务怎么配置都不生效,确认下你的存储引擎是否支持事务。
三、开发使用不当引发
7、异常被方法内部try catch捕获,没有重新抛出
示例代码:
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Transactional
public int addOrder(Double amount, String address) {
int order = 0;
try {
order = orderMapper.addOrder(amount, address);
int i = order / 0;
} catch (Exception e) {
}
return order;
}
}
当外部接口调用addOrder()方法,异常发生的时候,数据库数据正常入库了,事务未生效。
在TransactionAspectSupport.invokeWithinTransaction()方法中,我们可以看到以下逻辑。
try catch 会监控invocation.proceedWithInvocation()方法的执行,也就是目标方法的执行结果,如果我们的目标方法addOrder()手动捕获了异常,没有抛出,在invokeWithinTransaction方法中的try catch就无法捕获到,它会认为逻辑正常,最终会调用commitTransactionAfterReturning()提交事务,从而导致事务控制失效。
8、嵌套事务回滚多了
示例代码:
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Autowired
private SysUserServiceImpl sysUserService;
@Transactional(rollbackFor = Exception.class)
public int addOrder(Double amount, String address){
int order = orderMapper.addOrder(amount, address);
sysUserService.saveUser();
return order;
}
}
@Service
public class SysUserServiceImpl {
@Transactional
public void saveUser(){
throw new RuntimeException("新增人员失败");
}
}
存在以上场景,即使sysUserService.saveUser()方法发生异常,我们期望orderMapper.addOrder()方法执行的结果也正常入库。
当出现嵌套事务发生异常的时候,实际上两个方法的事务都会进行回滚。
解决办法:
1、addOrder()方法使用trye/catch包住
2、saveUser()方法的事务传播机制调整为Propagation.REQUIRES_NEW
以上两种方式都可以实现我们需要的场景。
9、rollbackFor属性设置错误
示例代码:
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Transactional
public int addOrder(Double amount, String address) throws FileNotFoundException {
int order = orderMapper.addOrder(amount, address);
throw new FileNotFoundException("11111");
}
}
在demo中,我们手动的抛出FileNotFountException异常,这是一个IOException异常。
当我们使用@Transactional,在未对rollbackFor做配置的情况下,默认是支持对Runtime和Error异常的回滚的。
但当我们的demo中的异常是IOException的时候。
DefaultTransactionAttribute.rollbackOn()方法返回false,就会导致completeTransactionAfterThrowing()方法调用694行逻辑txInfo.getTransactionManager().commit(txInfo.getTransactionStatus())提交事务。
从源码694行else逻辑的注释上我们也能看出,无法回滚异常。
所以通常情况下,我们建议指定@Transactional(rollbackFor = Exception.class)的方式进行异常捕获。
10、设置不支持事务的传播机制
Spring支持了7种传播机制,分别为:
上面不支持事务的传播机制为:PROPAGATION_SUPPORTS,PROPAGATION_NOT_SUPPORTED,PROPAGATION_NEVER。
如果配置了这三种传播方式的话,在发生异常的时候,事务是不会回滚的。
示例代码:
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Transactional(rollbackFor = Exception.class, propagation = Propagation.SUPPORTS)
public int addOrder(Double amount, String address) throws FileNotFoundException {
int order = orderMapper.addOrder(amount, address);
throw new FileNotFoundException("11111");
}
}
在处理事务异常回滚AbstractPlatformTransactionManager.processRollback()的逻辑中,这三种传播机制,就只是打印了下debug日志,没有进行真正的回滚,从日志记录信息中我们可以看到:
logger.debug("Should roll back transaction but cannot - no transaction available");
应该回滚但是不能,因为没有可用的事务。