大家好,我是一名CV工程师。
作为一个Java开发程序员,或者说作为一个Springer或者SpringBooter。应该很少有人手动管理事务(显式使用START TRANSACTION;、COMMIT;、ROLLBACK;开启、提交、回滚事务)。99%都是将需要事务的方法上加上@Transactional注解,将事务交由Spring管理。
这样做既使得代码变得美观还减少了代码的耦合度,让程序员更专注于业务的开发。但同时也让问题的排查变得更困难。比如我明明在方法上加了这个注解,为什么方法出现异常了,数据库的更新操作并没有回滚。一旦出现这样的问题,很多人就会毫无头绪。那么接下来我将为大家具体分析@Transactional注解的几大原因以及解决方案。
在分析原因之前,我觉得大家还是有必要了解下Spring的声明式事务的实现原理和
@Transactional注解的各个属性以及属性的作用。
Spring声明式事务的实现原理
@Transactional是基于aop实现的,那么必定有拦截器和切面。TransactionInterceptor会拦截加了@Transactional注解的方法,在调用目标方法前,会调用TransactionAspectSupport的invokeWithinTransaction方法对目标方法进行增强--对目标方法执行前,创建事务和数据库连接,在目标方法执行后,提供事务的提交和回滚。
@Transactional注解属性
package org.springframework.transaction.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 指定事务管理器的名称,当有多个事务管理器时需要指定
@AliasFor("transactionManager")
String value() default "";
//同 value
@AliasFor("value")
String transactionManager() default "";
//事务传播行为,默认是REQUIRED
Propagation propagation() default Propagation.REQUIRED;
//事务隔离级别,默认是DEFAULT(使用数据库默认隔离级别)
Isolation isolation() default Isolation.DEFAULT;
//事务超时时间,单位秒,默认是-1(使用数据库默认超时设置)
int timeout() default -1;
//是否只读事务,默认false
boolean readOnly() default false;
//指定哪些异常会触发事务回滚
Class<? extends Throwable>[] rollbackFor() default {};
//指定哪些异常类名会触发事务回滚
String[] rollbackForClassName() default {};
//指定哪些异常不会触发事务回滚
Class<? extends Throwable>[] noRollbackFor() default {};
//指定哪些异常类名不会触发事务回滚
String[] noRollbackForClassName() default {};
}
我们对声明式事务的实现原理以及@Transactional注解属性有了基本了解之后,我们再来看有哪些场景会导致注解失效,就可以很快的理解其失效原因了。
一、方法内自调用
这是@Transactional注解失效的最常见原因。从上面Spring声明式事务的实现原理我们了解到是基于SpringAOP实现的。当在同一个类中的方法自调用时,不会使用Java反射调用方法,所以事务也就不会生效。
@Service("test1Service")
public class Test1Service {
@Resource
private Test1Mapper test1Mapper;
public void update2(){
this.update();
}
/**
* 调用方法
*/
@Transactional(rollbackFor = Exception.class)
public void update() {
test1Mapper.updateId1ColTo2();
throw new RuntimeException("模拟异常");
}
}
以上这段代码,当在controller中直接调用update方法时,更新操作可以正常被回滚。是当在controller中调用update2方法,在update2方法中再直接调用update方法,就不会回滚。
解决方案
- 注入自身代理对象
@Service("test1Service")
public class Test1Service {
@Resource
private Test1Mapper test1Mapper;
//注入自身代理
@Resource
private Test1Service test1Service;
public void update2(){
//不使用this调用,而使用自身代理对象调用
test1Service.update();
}
/**
* 调用方法
*/
@Transactional(rollbackFor = Exception.class)
public void update() {
test1Mapper.updateId1ColTo2();
throw new RuntimeException("模拟异常");
}
}
2. 将方法移到另一个类中
这个就不累赘了,就是将需要事务管控的方法移到一个其他类中,再通过其他类的代理对象调用。
- 通过 ApplicationContext 获取代理
@Service("test1Service")
public class Test1Service {
@Resource
private Test1Mapper test1Mapper;
@Resource
private ApplicationContext applicationContext;
public void update2(){
//通过applicationContext获取当前类的代理对象
Test1Service test1Service =
applicationContext.getBean(Test1Service.class);
test1Service.update();
}
/**
* 调用方法
*/
@Transactional(rollbackFor = Exception.class)
public void update() {
test1Mapper.updateId1ColTo2();
throw new RuntimeException("模拟异常");
}
}
二、@Transactional注解作用在非public方法上
@Service("test1Service")
public class Test1Service {
@Resource
private Test1Mapper test1Mapper;
@Resource
private Test1Service test1Service;
public void update2(){
//虽然是使用自身代理调用同类的方法,但是更新依旧没有回滚
test1Service.update();
}
/**
* 调用方法
*/
@Transactional(rollbackFor = Exception.class)
void update() {
test1Mapper.updateId1ColTo2();
throw new RuntimeException("模拟异常");
}
}
因为分布式事务是基于AOP实现的,而AOP是基于代理和反射实现的,在TransactionInterceptor (事务拦截器)中的目标方法执行前后进行拦截,DynamicAdvisedInterceptor的拦截器方法或者JdkDynamicAopProxy的invoke方法会间接调用AbstractFallbackTransactionAttributeSourcecomputeTransactionAttribute方法,获取@Transactional注解的事务配置信息。此方法会检查目标方法的修饰符是否为public,而不是public则不会获取@Transactional的属性配置信息,但是不会报错。
感兴趣的朋友可以研究下Spring关于声明式事务的源码
这个解决办法也很简单,将方法权限修饰词改为public就可以了。
三、@Transactional注解propagation属性设置错误
@Transactional注解的propagation属性是用来配置该方法与调用方方法的事务的传播机制的,涉及情况比较多,我单独写了一篇关于Spring声明式事务的传播机制的介绍文章。
传送门:[ ](Spring声明式事务:@Transactional用起来简单,但传播机制你真的配明白了吗?在使用Spring声明式事务 - 掘金)
四、@Transactional注解rollbackFor或者noRollbackFor属性设置错误
不知道这两个属性作用的朋友往上滑滑,上面介绍了@Transactional所有属性的作用。
@Service("test1Service")
public class Test1Service {
@Resource
private Test1Mapper test1Mapper;
@Transactional
public void update() throws Exception {
test1Mapper.updateId1ColTo2();//不会回滚
throw new Exception("模拟异常");
当@Transactional注解未指定rollbackFor的属性时,默认回滚所有的RuntimeException。其他异常比如IOException这类的检查时异常默认不会回滚。
@Service("test1Service")
public class Test1Service {
@Resource
private Test1Mapper test1Mapper;
/**
* 调用方法
*/
@Transactional(rollbackFor = IOException.class, noRollbackFor = RuntimeException.class)
public void update() {
test1Mapper.updateId1ColTo2(); //不会回滚
throw new RuntimeException("模拟异常");
}
}
以上示例中,明确指定了回滚IOException以及RuntimeException不回滚,所以更新操作也不会被回滚。
五、在事务方法中使用 catch捕捉了异常,导致@Transactional失效
如果在方法中使用try-catch捕捉了出错的代码,那么异常也就会被消化掉,也不会触发回滚。
@Service("test1Service")
public class Test1Service {
@Resource
private Test1Mapper test1Mapper;
/**
* 调用方法
*/
@Transactional(rollbackFor = Exception.class)
public void update() {
test1Mapper.updateId1ColTo2();//不会回滚
try {
throw new RuntimeException("模拟异常");
} catch (Exception e) {
log.info("出现模拟异常");
}
}
}
解决办法
这时候如果需要回滚的话,可以在catch中将异常重新抛出。
@Service("test1Service")
public class Test1Service {
@Resource
private Test1Mapper test1Mapper;
/**
* 调用方法
*/
@Transactional(rollbackFor = Exception.class)
public void update() {
test1Mapper.updateId1ColTo2();
try {
throw new RuntimeException("模拟异常");
} catch (Exception e) {
log.info("出现模拟异常");
//将异常再次抛出
throw new BusinessException("自定义业务异常");
}
}
}
六、数据库引擎不支持事务
可以使用 SHOW ENGINES;命令查看MySQL支持事务的引擎。
可以看出来我的版本只有InnoDB引擎支持事务操作。
关注我,后续为大家带来更多知识讲解以及高频率的面经!!!