【4】Spring源码-Transaction

5. Transaction

  1. 默认情况下spring中的事务处理只对RuntimeException方法进行回滚,所以,如果此处将RuntimeException替换成普通的Exception不会产生回滚效果。
  2. Spring中的声明式事务是以aop为基础的(对原方法进行前后拦截)。编程式事务则是使用TransactionTemplate
  3. 在ApplicationContext从BeanFactory中加载所有Bean时,Spring创建被代理的对象(ApplicationContext非延迟加载,可以在启动时知道bean是否能被加载成功)。这时候,所有符合在pointcut中配置的类的相关方法就会被织入切面代码,并且返回相关的动态代理对象。实现BeanPostProcessor后,在getBean时,spring会遍历所有已经注册的BeanPostProcessor,调用其postProcessAfterInitialization方法,在该方法中会寻找已经注册过的事务的增强器。
  4. 当代理类被调用时会调用这个类的增强方法,也就是bean的Advise,又因为在定义解析事务标签时spring把TransactionInterceptor类型的bean注入到了BeanFactoryTransactionAttributeSourceAdvisor中,所以在调用事务增强器增强的代理类时会首先执行TransactionInterceptor进行增强,同时,也就是在TransactionInterceptor类中的invoke方法中完成了整个事务的逻辑(在调用到被@Transactional注释的方法时才会调用到invoke方法)。
  5. @Transactional注解,如果实现类方法中存在事务属性,则使用实现类方法上的属性,否则使用所在类上的属性,如果实现类方法所在类的属性上还是没有搜索到对应的事务属性,那么再搜索被实现接口中的方法,如果再没有的话,最后尝试搜索被实现接口的接口类上面的声明。spring代码中就是通过反射,按照这样的顺序查找的。

5.1 声明式事务处理流程

在Spring中支持两种事务处理的方式,分别是声明式事务处理与编程式事务处理,两者相对于开发人员来讲差别很大,但是对于Spring中的实现来讲,大同小异。在invoke中我们也可以看到这两种方式的实现。对于声明式的事务处理主要有以下几个步骤。

  1. 获取事务的属性。
    对于事务处理来说,最基础或者说最首要的工作便是获取事务属性了,这是支撑整个事务功能的基石,如果没有事务属性,其他功能也无从谈起。
  2. 加载配置中配置的TransactionManager。
  3. 不同的事务处理方式使用不同的逻辑。(编程式的事务处理是不需要有事务属性)
  4. 在目标方法执行前获取事务(createTransactionIfNecessary)并收集事务信息。
    事务信息与事务属性并不相同,也就是TransactionInfo与TransactionAttribute并不相同, TransactionInfo中包含TransactionAttribute信息,但是,除了TransactionAttribute外还有其他事务信息,例如PlatformTransactionManager以及TransactionStatus相关信息。
  5. 执行目标方法。(这里的执行目标方法,还会去继续调用拦截器链上的其他方法,事务相当于一个around advice
  6. 一旦出现异常,尝试异常处理。
    并不是所有异常,Spring都会将其回滚,默认只对RuntimeException回滚。
  7. 提交事务前的事务信息清除。
  8. 提交事务。
// TransactionInterceptor#invoke -> TransactionAspectSupport#invokeWithinTransaction
// 声明式事务处理
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
   // 创建TransactionInfo
   TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

   Object retVal;
   try {
      // 执行拦截器链中的其他方法,事务相当于一个around advice
      retVal = invocation.proceedWithInvocation();
   }
   catch (Throwable ex) {
      // 异常回滚
      completeTransactionAfterThrowing(txInfo, ex);
      throw ex;
   }
   finally {
      // 清除信息
      cleanupTransactionInfo(txInfo);
   }
   // 提交事务
   commitTransactionAfterReturning(txInfo);
   return retVal;
}

5.1.1 创建事务createTransactionIfNecessary

createTransactionIfNecessary整体流程:

  1. 获取事务,如果当前线程存在事务,则转向嵌套事务处理;
  • 按事务传播行为进行区分:
        a. PROPAGATION_REQUIRES_NEW表示当前方法必须在它自己的事务里运行,一个新的事务将被启动,而如果有一个事务正在运行的话,则在这个方法运行期间被挂起。对于PROPAGATION_REQUIRES_NEW,将原事务挂起并将信息记录在TransactionStatus中,对于挂起操作的主要目的是记录原有事务的状态,以便于后续操作对事务的恢复;(通过TransactionStatus(后面被设置进TransactionInfo中)进行回滚
        b. 对于PROPAGATION_NESTED,则使用JDBC 3.0提供的Savepoints API设置保存点。被嵌套的事务可以独立于封装事务进行提交或者回滚,如果封装事务不存在,行为就像PROPAGATION_REQUIRES_NEW。(通过Savepoints进行回滚)对于嵌入式事务的处理,Spring中主要考虑了两种方式的处理。
            i. Spring中允许嵌入事务的时候,则首选设置保存点的方式作为异常处理的回滚。
            ii. 对于其他方式,比如JTA无法使用保存点的方式,那么处理方式与PROPAGATION_REQUIRES_NEW相同,而一旦出现异常,则由Spring的事务异常处理机制去完成后续操作。
  1. 如果不存在则从jdbc获取数据库连接,新建事务,设置事务隔离级别(spring默认的事务隔离级别是跟JDBC相同的,即默认情况Spring不设置事务隔离级别);
  2. 取消事务自动提交,由spring控制;
  3. 设置TransactionInfo,这个实例中包含了目标方法开始前的所有状态信息(将TransactionStatus设置进去),一旦事务执行失败,spring会通过TransactionInfo中的信息来进行回滚等后续工作。(事务信息记录在当前线程中。绑定在一个theadlocal中)

5.1.2 invocation.proceedWithInvocation()

继续执行拦截器链,当有其他拦截器match待执行方法时,则执行该拦截器方法,然后return。如果没有match则递归调用。

5.1.3 回滚处理completeTransactionAfterThrowing

  1. 判断抛出异常的类型,如果是RuntimeException或Error则会回滚。默认情况下Exception不会回滚,数据仍然会被commit;若想要Exception也被回滚,则可以使用@Transaction(rollbackFor=Exception.class);
  2. 当之前已经保存的事务信息中有保存点信息时,使用保存点Savepoints信息进行回滚。常用于嵌入式事务,对于嵌入式的事务的处理,内嵌的事务异常并不会引起外部事务的回滚。使用JDBC的connection根据Savepoints进行回滚,具体的操作在由使用的数据库连接池封装。回滚后会清除Savepoints;
  3. 当之前已经保存的事务信息中的事务为新事物,那么直接回滚。常用于单独事务的处理。对于没有保存点的回滚(有TransactionStatus),Spring同样是使用底层数据库连接提供的API来操作的。由于我们使用的是DataSourceTransactionManager,那么doRollback函数会使用此类中的实现
  4. 当前事务信息中表明是存在事务的,又不属于以上两种情况,多数用于JTA,只做回滚标识,等到提交的时候统一不提交
  5. 进行回滚后信息的清除。(释放数据库连接,恢复数据库连接的自动提交属性,如果在本事务执行前有事务挂起的,则当前事务执行完毕后会恢复挂起的事务

5.1.4 事务提交commitTransactionAfterReturning

  1. 在真正的数据提交之前,还需要做个判断。在事务异常处理规则的时候,当某个事务既没有保存点又不是新事物,Spring对它的处理方式只是设置一个回滚标识。这个回滚标识在这里就会派上用场了,主要的应用场景如下。
  2. 某个事务是另一个事务的嵌入事务,但是,这些事务又不在Spring的管理范围内,或者无法设置保存点,那么Spring会通过设置回滚标识的方式来禁止提交。首先当某个嵌入事务发生回滚的时候会设置回滚标识,而等到外部事务提交时,一旦判断出当前事务流被设置了回滚标识,则由外部事务来统一进行整体事务的回滚。
  3. 在提交过程中也并不是直接提交的,而是考虑了诸多的方面,符合提交的条件如下。只有是新事务才会直接提交(没有设置保存点)
    a. 当事务状态中有保存点信息的话便不会去提交事务。
    b. 当事务非新事务的时候也不会去执行提交事务操作。
  4. 此条件主要考虑内嵌事务的情况,对于内嵌事务,在Spring中正常的处理方式是将内嵌事务开始之前设置保存点,一旦内嵌事务出现异常便根据保存点信息进行回滚,但是如果没有出现异常,内嵌事务并不会单独提交,而是根据事务流由最外层事务负责提交,所以如果当前存在保存点信息便不是最外层事务,不做保存操作,对于是否是新事务的判断也是基于此考虑。
  5. 如果程序流通过了事务的层层把关,最后顺利地进入了提交流程,那么同样,Spring会将事务提交的操作引导至底层数据库连接的API,进行事务提交。
  6. 如果提交事务的过程中出现也异常,也会进行回滚。

5.2 Spring事务传播行为

Spring中事务默认的传播行为:REQUIRED


image.png

嵌套事务:ServiceA的方法A调用ServiceB的方法B

  1. REQUIRED:A、B无论哪个发生异常,都A、B的操作都将被回滚

  2. REQUIRES_NEW,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。如果B异常,A捕获了该异常,则A不会回滚,如果A没有捕获该异常,则A会回滚。A事务异常,B事务是不会回滚的。
    a. REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。

  3. NESTED,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用jdbc的savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTATrasactionManager实现可能有不同的支持方式。
    a. NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint。嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。

5.3 内部方法调用事务不生效

@Slf4j
@Service
public class UserServiceImpl implements UserService{

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserService userService;

    /**
     * 方法1 在调用类上添加事务
     */
    @Transactional(rollbackFor = Exception.class)
    public void testRollback() {

        User user = new User();

        user.setPassword("password1");

        user.setUsername("username1");

        userMapper.insert(user);

        /**
         * 方法2
         * @Autowired

         * private UserService userService;

         * 自己注入自己,事务可以生效
         */
        userService.testRollback2();

        /**
         * 方法3
         * 将内部事务暴露出来,事务可以生效
         * spring boot启动类上添加注解@EnableAspectJAutoProxy(exposeProxy = true),或使用xml配置
         */
        ((UserService)AopContext.currentProxy()).testRollback2();

        /**
         * 直接调用,事务不生效
         */
        testRollback2();

    }

    @Transactional(rollbackFor = Exception.class)
    public void testRollback2() {

        User user = new User();

        user.setPassword("password2");

        user.setUsername("username2");

        userMapper.insert(user);

        throw new RuntimeException("haha");

    }

}
  1. 在spring内部调用可能会存在事务失效的问题,主要是spring事务是通过代理模式实现的,在一个service里面如果方法A(未开启事务)调用方法B(有事务注解@Transactional)时方法B的事务是不起作用的,这种情况是因为方法A未开启事务没有触发代理,在内部调用方法B,this.B()这种调用,也不会触发代理对象去增强B方法,结果就是方法B事务失效。

  2. 调用A方法时,内部方法B的事务@Transactional不会生效,A的事务会生效;
    a. 因为事务基于aop实现,aop是方法级别的增强,在调用A方法时,检测到有Transactional注解,会被拦截器拦截,A方法会被代理,也是就在A方法执行前后会有增强advice,但是B方法没有被代理还是原来的方法,所以Transactional注解不会生效。
    b. 但是直接执行B方法,Transactional是会生效的,因为被代理了;
    c. 而将类本身注入后,则调用service.B()则可以使用事务;

  3. 方法1 在调用方法testRollback上添加事务,testRollback2的事务还是不会生效,只是testRollback的事务生效了。username1和username2都会回滚

  4. 如果exposeProxy = true,AopContext.currentProxy() 用threadLocal记录当前的代理类

  5. 事务回滚后,再次插入记录时会发现mysql注解id已经自增过一次了

  6. 使用方法3时,需要添加依赖


   org.aspectj
   aspectjweaver
   1.9.1

  1. spring自己注入自己时,去缓存中拿到自己,就不会无限调用getBean。

你可能感兴趣的:(【4】Spring源码-Transaction)