这是一道非常常见的面试题了,了解这个对于我们平常开发也是很有帮助的,下面总结一下导致事务失效的场景。
一、内部方法调用
比如我们看如下的例子,我们的Teacher类,假如执行这个类中的saveData()方法,这个方法上没有加事务注解,这个方法内部调用了batchInsert();方法,这个batchInsert()方法上面有@Transactional注解。这种场景下事务是不生效的。
1.1 失败原因
像Teacher类被spring托管创建时,是以CGLIB动态代理的方式生成代理对象,但是由于一开始调用的是非事务方法,非事务方法内部调用带事务的方法,这时真正去执行batchInsert()方法是被代理的这个Teacher类本身,并不是代理对像,因此事务失效了。
package com.example.bean;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class Teacher {public void saveData() {this.batchInsert();}@Transactionalpublic void batchInsert() {//真正执行保存数据}
}
1.2 解决方案
那么如何解决这个问题?常见有两种解决方案
1.2.1 事务方法剥离到其他类
将batchInsert()方法单独拆分到另一个类中,比如CommonSaveData类,如下所示
package com.example.bean;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class CommonSaveData {@Transactionalpublic void batchInsert() {//真正执行保存数据}
}
而Teacher类代码如下,可以看到把CommonSaveData类通过依赖注入的方式给注入进来了,然后通过调用commonSaveData.batchInsert();来保存数据。这种场景事务就生效了,因为我们此时拿到的CommonSaveData对象是代理对象,而且是我们这时调用不再是同类调用了,跨类调用,并且这个方法上有@Transactional注解,事务是有效的。
package com.example.bean;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class Teacher {@Autowiredprivate CommonSaveData commonSaveData;public void saveData() {commonSaveData.batchInsert();}}
1.2.2 在同类中获取代理对象
要达到目的,首先我们得在这个类上加@EnableAspectJAutoProxy(exposeProxy = true)注解,作用是启用暴露代理对象。
然后就是通过(Teacher)AopContext.currentProxy();这样获取到Teacher类的代理对象,然后由代理对象去调用带有事务注解的方法batchInsert(),此时事务就是有效的。
package com.example.bean;import org.springframework.aop.framework.AopContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
@EnableAspectJAutoProxy(exposeProxy = true)
public class Teacher {public void saveData() {Teacher proxy = (Teacher) AopContext.currentProxy();proxy.batchInsert();}@Transactionalpublic void batchInsert() {//真正执行保存数据}
}
二、异常未正常抛出
2.1 失效原因
我们在加事务注解的方法中,默认情况只有抛出的异常是RuntimeException或者Error异常时才会回滚,假如我们抛出的异常是其它类型,那么默认情况下会导致事务失效,如下例子。
package com.example.bean;import org.springframework.aop.framework.AopContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeoutException;@Service
@EnableAspectJAutoProxy(exposeProxy = true)
public class Teacher {public void saveData() throws TimeoutException {Teacher proxy = (Teacher) AopContext.currentProxy();proxy.batchInsert();}@Transactionalpublic void batchInsert() throws TimeoutException {//真正执行保存数据throw new TimeoutException();}
}
2.2 解决方案
像下面代码,我们在@Transactional注解中添加了(rollbackFor = TimeoutException.class)这样就可以覆盖spring默认事务回滚异常策略,从而当抛出TimeoutException时也支持事务回滚。
如果你这个带事务的方法想要在任何异常场景都自动回滚,那么就直接设置成@Transactional(rollbackFor = Exception.class)
package com.example.bean;import org.springframework.aop.framework.AopContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeoutException;@Service
@EnableAspectJAutoProxy(exposeProxy = true)
public class Teacher {public void saveData() throws TimeoutException {Teacher proxy = (Teacher) AopContext.currentProxy();proxy.batchInsert();}@Transactional(rollbackFor = TimeoutException.class)public void batchInsert() throws TimeoutException {//真正执行保存数据throw new TimeoutException();}
}
三、非public方法
如果我们事务注解加到了私有方法上,如下所示,batchInsert方法是protected修饰的,不是public,这种情况事务会失效,失效原因如下。
3.1 失效原因
3.1.1 CGLIB代理的继承限制
CGLIB通过生成目标类的子类作为代理对象,并重写父类的public方法来实现拦截。由于非public方法无法被重写,导致代理对象无法插入事务逻辑。
3.1.2 Spring AOP的默认行为
Spring AOP默认仅拦截public方法,这是出于性能和安全性的考虑。
package com.example.bean;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class Teacher {@Transactionalprotected void batchInsert() {//真正执行保存数据}
}
解决方案:
上面的例子中只要将protected改为public即可。
四、数据库引擎不支持索引
4.1 失效原因
我们一般开发场景中数据库设计时都是使用innodb作为引擎的,但是可能有些特殊场景的表使用的是myisam引擎,我们都知道数据库中myisam引擎是不支持事务的。
4.2 解决方案:
将数据库引擎由myisam改为innodb即可解决问题。
五、未启用事务管理
5.1 失效原因
我们在spring配置类中没有添加@EnableTransactionManagement注解,这样的话,就是没有开启事务功能,这种情况我们无论如何加@Transactional注解都是无效的了。
5.2 解决方案
在我们的主配置类上加上@EnableTransactionManagement注解,即可开启事务支持,从而解决事务不生效的问题。
六、传播行为不当
6.1 失效原因
我们默认给事务注解添加的传播属性不准确,如下所示,在batchInsert方法上,事务注解指定的传播属性是Propagation.NEVER,这时事务会被禁止。还有一个是将传播属性设置为了Propagation.NOT_SUPPORTED,这个是将事务挂起了。
package com.example.bean;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeoutException;@Service
public class Teacher {@Transactional(propagation = Propagation.NEVER)protected void batchInsert() throws TimeoutException {//真正执行保存数据}
}
6.2 解决方案
我们修改传播行为Propagation.REQUIRED,这样就可以解决事务失效的问题了
七、异常被捕获未抛出
7.1 失效原因
在try catch中捕获异常但并未再抛出,事务无法感知异常导致事务失效。
7.2 解决方案
我们在捕获到异常后,要么重新抛出,要么在catch中自己处理回滚逻辑。
八、类未被spring管理
8.1 失效原因
比如我们上面提到的Teacher类上没有加@Service注解,这样spring无法自动帮我们管理这个bean,也就无法自动帮我们生成代理类,或者我们不是拿的spring帮我们生成的Teacher对象,而是自己又通过代码new 出来了一个Teacher对象,这个new出来的对象不是代理对象,因此也就事务失效了。
8.2 解决方案
我们在我们的Teacher类上正常加上@Service注解,让它被spring容器纳管,这样便可以解决事务失效问题。
九、多线程场景
9.1 失效原因
假如在我们一个大的事务中,通过线程池异步去处理一些业务逻辑了,那么假如主线程发生异常,那么被线程处理的数据是无法发生回滚的。同理,如果是线程里面出现失败回滚,但主线程是无法回滚的。因为线程中的事务跟主线程中的事务压根就不是同一个。
9.2 解决方案
避免跨线程操作,或者使用成熟的分布式事务框架来解决,比如Seta、TCC等。
以上便是最常见的事务失效的场景。