目录
2.3.1.TransactionInterceptor如何拦截加了@Transactional注解的方法?
1.概述spring事务的原理
spring事务的原理是什么?
首先mysql这样的数据库本身是支持事务的,有不同的事务隔离级别,事务分为手动开启事务和自动开启事务,通过底层的支持,可以实现多条sql 原子化,要么都执行,要么都不执行。spring事务本质上也是依赖底层数据库的事务机制来实现的。
spring事务分为编程式事务和声明式事务(包括注解或者xml配置),此处只讲注解式事务。
本质就是拦截被注解标识的方法,生成代理对象,把默认的自动开启事务变为手动开启,这样多条sql语句都执行完后,才会提交事务。如果有某条执行失败,则执行回滚操作。
2.@Transactional注解的原理
2.1 整体时序图
2.2 整体核心流程:
1.TransactionInterceptor负责拦截加了@Transactional注解的方法;
2.TransactionAspectSupport负责对目标方法增强
2.1 在目标方法前创建事务和数据库连接;
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
2.2 执行目标方法 retVal = invocation.proceedWithInvocation();
2.3 在目标方法报错后执行回滚或者加回滚标识(设置connectionHolder的rollbackOnly为true)
completeTransactionAfterThrowing(txInfo, ex);
2.4 在目标方法执行成功后执行提交或者回滚(如果检查到connectionHolder的rollbackOnly为true,就会执行回滚,不然就执行提交)
commitTransactionAfterReturning(txInfo);
2.3 各个环节详细代码分析
核心逻辑在TransactionAspectSupport#invokeWithinTransaction这个方法标红的5处,接下来会分别详细的介绍这5个子方法。
2.3.1.TransactionInterceptor如何拦截加了@Transactional注解的方法?
在spring容器启动的时候:AbstractAutoProxyCreator(继承自SmartInstantiationAwareBeanPostProcessor),BeanPostProcessor 的意思就是 Bean 的后置处理器.主要作用就是帮助我们在bean实例化.依赖注入完毕之后,初始化前后做一些事情。AbstractAutoProxyCreator在bean创建后,初始化前会调用postProcessBeforeInstantiation此方法,此方法会获取所有要加强的通知,生成代理类:
最终spring的事务切点TransactionAttributeSourcePointcut的matches方法,会匹配每一个调用的方法,找到有@Transactional注解的方法。
重点看一下org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource#computeTransactionAttribute真正获取注解的封装事务属性的方法:computeTransactionAttribute
其方法第一步:会判断方法是否是public方法,不是public方法,就直接返回null.
就是图中标1的地方。此处很重要,说明被@Transaction修饰的非public方法事务是不会生效的,因为此方法都不会被拦截。
接下来看具体解析注解的方法:
这里会找到Transactional注解属性,然后解析注解上的属性字段。
AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
element, Transactional.class, false, false);
if (attributes != null) {
return parseTransactionAnnotation(attributes);
}
下面会真正的解析注解上的属性,会把属性数据封装到TransactionAttribute对象上。包括事务的传播属性,隔离级别,超时时间,rollbackFor等。
rollbackFor如果没有指定默认只能回滚RunTimeException.指定了异常,异常也必须是Throwable的子类。具体代码分析在异常回滚代码分析中会细说。
至此在spring容器启动的时候,拦截带@Transaction注解的方法,生成代理类。并解析注解中配置的属性数据,并封装到TransactionAttribute对象上代码分析完毕。
2.3.2.在目标方法执行前创建事务和数据库连接
接下来开始分析在目标方法之前创建事务和数据库连接的代码。spring的事务本质上是通过底层数据库的事务完成的。所以核心点是整个事务过程中要使用的是同一个数据库连接,才能保证事务的执行。
接下来开始分析spring具体创建事务和数据库连接的代码:
TransactionAspectSupport负责对目标方法增强,在目标方法前创建事务和数据库连接;
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
这里tm是DataSourceTransactionManager,所以会进入图中标红的地方:
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
接下来看具体的createTransactionIfNecessary创建事务的地方:
最核心的方法是status = tm.getTransaction(txAttr);,
getTransaction方法中最新的就是doBegin方法。
最核心的是doBegin方法:
实际调用的DataSourceTransactionManager的doBegin方法,做了下面几件事:
1.调用dataSource.getConnection方法获取数据库连接connection
2.将connection封装入ConnectionHolder,
3. 并设置synchronizedWithTransaction为true,并将connectionHolder封装到datasourceTransactionManager.dataSourceTransactionObject类型的txOBject对象里面
4.调用dataSourceUtils.prepareConnectionForTransaction方法从注解属性中获取相关值并设置相关属性:
3.1设置数据库连接的事务隔离级别transactionalIsolation,
3.2设置自动提交autoCommit为false,
3.3设置timeout事务过期时间,
3.4设置transactionActive为true
5.将connectionHolder存入TransactionSynchronizationManager的resources,以dataSources为key。后续事务获取数据库连接都是从TransactionSynchronizationManager的resources中获取了,这样就保证了整个事务中获取的都是同一个数据库连接。
接下来上doBegin的具体代码:
接下来看将connectionHolder存入TransactionSynchronizationManager的resources的源码:
可以看到本质上就是key为:以dataSources为key,value为connectionHolder.把数据放入到了TransactionSynchronizationManager对象的resources属性中。
至此创建事务和获取数据库连接等核心代码分析完毕。
2.3.3. 执行目标方法
调用目标方法,这个就是mybatis的内容了,mybatis执行的时候会获取数据库连接。
还是有需要提到的地方就是在调用数据库的时候SpringManagedTransaction类的openConnection方法获取数据库连接。
从上述代码中可以看到最终的数据库连接是从TransactionSynchronizationManager的线程变量resources里面获取connectionHolder,肯定是可以获取到的
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
由此验证了:同一个事务里面,共用connectionHolder,所以同一个事务里面,肯定是共用一个数据库连接的;
2.3.4. 在目标方法报错后执行回滚或者加回滚标识
可以看到在目标方法执行抛异常了,事务切面类TransactionAspectSupport会catch住异常,执行
completeTransactionAfterThrowing(txInfo, ex);事务回滚方法。
接下来看completeTransactionAfterThrowing此方法的具体内部逻辑:
先看txInfo.transactionAttribute.rollbackOn(ex)方法:
如果注解上指定了rollbackFor属性,则走第一步标红的地方。
否则走第二步,我们重点看一下第二步?来猜一下如果没有指定会回滚吗?
我们发现如果没有指定那么只能回滚RuntimeException或者Error异常,其他异常是无法回滚的。所以使用的时候一定要注意。
接下来看具体的回滚逻辑:txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
再接着看processRollBack方法。
我们可以看到如果是新事物,则直接处理回滚,
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
doRollback(status);
}
本质上是获取数据库连接,通过数据库是进行回滚。
如果分析一下如果是已有事务,出现了异常会怎么样?
如果是已有事务,可以看到只是把事务的getconnectionHolder中的属性的rollbackonly设置为true:
那有2个问题
1.什么情况下属于已有数据,会进入此分支呢?
2.设置为rollbackonly设置为true,那最终什么时候会回滚呢?
先回答第一个问题,先看下面的2个方法:在执行methodB方法的时候会报错,就是进入上述的已有事务出现异常的分支中:
@Transactional
public void methodA(){
userMapper.selectById("jzcs");
//methodB会报错
infoService.getUserClassInfo("userId");
}
//methodB是infoService的方法
@Transactional
public void methodB(String userId){
infoMapper.selectUserClassInfo(userId);
//这个地方肯定会报错
System.out.println(1/0);
}
DEBUG调式信息:
可以分析在methodB报错的时候,会设置rollbackonly为true,在methodB会把异常抛给methodA,methodA执行结束后,会抛异常,最终会走到:
这里进行异常回滚。
那么再思考一个问题,如果methodA try,catch住了methodB的异常会怎样呢?
@Transactional
public void methodA(){
userMapper.selectById("jzcs");
try {
//methodB会报错,但是这里被try catch了,所以methodA就检查不到错误
infoService.getUserClassInfo("userId");
}catch (Exception e){
e.printStackTrace();
}
}
//methodB是infoService的方法
@Transactional
public void methodB(String userId){
infoMapper.selectUserClassInfo(userId);
//这个地方肯定会报错
System.out.println(1/0);
}
留个悬念,大家猜一下methodA捕获了B方法的异常,那会回滚吗?大家先自行思考一下,后面会揭晓答案。
2.3.5 在目标方法执行成功后执行提交或者回滚
接下来看:TransactionAspectSupport#commitTransactionAfterReturning方法。
此方法的作用是:
在目标方法执行成功后执行提交或者回滚(如果检查到connectionHolder的rollbackOnly为true,就会执行回滚,不然就执行提交)
可以看到如果在rollbackonly属性为true的时候,就执行回滚,否则执行提交。
是不是就回答了上面的问题,rollbackonly属性为true的时候是在这里执行回滚的,所以上面的methodA捕获了B方法的异常,是会回滚的。
我们接下来具体看一下提交事务的代码:
可以看到是从connectionholder中获取数据库连接, 然后通过数据库连接实现的事务提交。
3.注解失效的场景
3.1 访问权限问题
我们知道java的访问权限有: public,protected,default,private四种。权限的权限从左到右,依次递减。
我们在分析2.1中spring拦截transaction事务注解的时候,分析到:
如果方法是非public的,即使使用了注解,也不会被拦截到的,自然事务就不会生效。
现在idea都会比较智能,如果是非public 方法,都会有错误提示的。所以可见非public方法事务是不会生效的。
3.2 方法用final或者static修饰的问题
我们知道spring事务的本质是通过aop代理,通过jdk动态代理或者cglib为我们生成代理类。如果方法被修改为final我们在代理中就无法重写该方法,就无法使用代理。可以看到我把方法该为final,idea编译器都提示无法被重写了。
再看static的问题,因为静态方法只能继承,无法重写,所以也无法使用事务。
3.3 方法内部调用
这种方法的内部调用事务是不会生效的。我们知道事务生效是通过spring aop代理,被目标方法生成代理类,内部方法调用使用的是this对象,this对象并不是代理对象,而是目标对象本身,所以事务肯定不会生效。
那内部调用如何才能让事务生效呢?有两种方式:
1)自己注入自己
2)通过AopContext.currentProxy获取当前类的代理的方式调用
3.4 未被spring管理
spring事务的前提是通过spring aop生成代理,在目前方法调用前生成代理类,通过代理类增强方法,在目标方法执行前创建事务,在目标方法执行成功或者失败的时候,进行提交或者回滚事务等。未被spring事务管理,肯定事务无法生效。
3.5 多线程调用
之前在分析源码的时候,在整个createTransactionIfNecessary()方法中:
1.获取数据库连接封装connectionHolder;
2.维护TransactionSynchronizationManager类里面的线程变量,比如将数据库连接信息存入线程变量resources中;后续被调用方法共用一个事务的话,就可以从resources里面获取相同的connectionHolder。这样就保证了同一个事务中公用的是同一个数据库连接。
同一个线程调用会把获取到的数据库连接存入到TransactionSynchronizationManager类里面的线程变量resources中,后续获取连接都是从此变量中获取。
因此多线程调用获取到的数据库连接都不是同一个数据库连接,自然不会生效。
3.6 错误的传播属性
事务的传播属性包括:
七个事务传播属性:
PROPAGATION_REQUIRED -- 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
PROPAGATION_SUPPORTS -- 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY -- 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW -- 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED -- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER -- 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED--如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
传播属性使用错误也会导致事务失效
3.7 自己catch了异常
如果在这里catch住了异常,那么在代理切面层就捕获不到异常了,
图中表中的就捕获不到异常,然后就会导致数据被提交,事务失效了。
3.8 手动抛了别的异常
上例中注解中并没有指定rollbackFor所以默认只能回滚RunTimeException,而代码捕获了异常,然后手动扔了Exception,则导致事务失效。
从源码分析一下为什么?
扔出异常后,会进去此方法:completeTransactionAfterThrowing(txInfo, ex);此方法会调用txInfo.transactionAttribute.rollbackOn(ex)方法进行判断,决定是否回滚。
rollbackon的具体代码:
super.rollbackon方法会判断只有RuntimeException或者Error异常才会进行回滚。
因为我们扔出来的是Exception,不满足要求,所以自然就不会回滚。
3.9 自定义了回滚异常
这个和3.8类型,不再赘述。