spring的注解式事务原理

目录

1.概述spring事务的原理

2.@Transactional注解的原理

2.1 整体时序图

2.2 整体核心流程:

2.3 各个环节详细代码分析

2.3.1.TransactionInterceptor如何拦截加了@Transactional注解的方法?

2.3.2.在目标方法执行前创建事务和数据库连接

2.3.3. 执行目标方法

2.3.4. 在目标方法报错后执行回滚或者加回滚标识

2.3.5  在目标方法执行成功后执行提交或者回滚

3.注解失效的场景

3.1 访问权限问题

3.2 方法用final或者static修饰的问题

3.3 方法内部调用

3.4 未被spring管理

3.5 多线程调用

3.6 错误的传播属性

3.7 自己catch了异常

3.8 手动抛了别的异常

3.9 自定义了回滚异常

3.10 嵌套事务回滚多了


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类型,不再赘述。

3.10 嵌套事务回滚多了

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值