• 浅析Spring事务实现原理


    SQL事务实现简介

    ​ 首先我们来了解下,最简单的事务是怎么实现的呢?以JDBC为例,当一个数据库Connection对象创建后,其会默认自动提交事务;每次执行SQL语句时,如果成功,就会向数据库自动提交,不能回滚。

    ​ 通过调用setAutoCommit(false)方法可以取消自动提交事务。等到所有的SQL语句都执行成功后,调用commit()方法提交事务。如果其中某个操作失败或出现异常时,则调用rollback()方法回滚事务。具体代码如下所示:

        public void noTransaction() {
            Connection connection = null;
            String sql = "update account set balance=balance-100 where id=1";
            String sql2 = "update account set balance=balance+100 where id=2";
            //创建PreparedStatement对象
            PreparedStatement preparedStatement = null;
            try {
                connection = JDBCUtils.getConnection();// 获取数据库连接
                connection.setAutoCommit(false);//事务开始
                preparedStatement = connection.prepareStatement(sql);
                preparedStatement.executeUpdate();//执行第一个sql
                preparedStatement = connection.prepareStatement(sql2);
                preparedStatement.executeUpdate();//执行sql2
                //提交事务
                connection.commit();
            } catch (SQLException e) {
                //进行事务回滚,默认回滚到事务开始的地方
                try {
                    connection.rollback();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
                e.printStackTrace();
            } finally {
                //关闭流
                JDBCUtils.close(null, preparedStatement, connection);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    ​ 将代码抽象成执行步骤,主要有以下四步:

    1. 获取Mysql链接
    2. 执行SQL语句
    3. 提交SQL事务
    4. 存在异常则做Mysql的事务回滚。

    ​ 可以发现,常规情况下只有执行SQL语句的内容存在差异。如果能将相同部分抽取出来,接入方接入时只考虑SQL语句内容,就可以减少接入的成本。同时观察到抽取的部分处于执行SQL语句的前后,那么很自然的就可以想到两种解决方案:

    ​ 1、在JAVA8中,提供了函数式编程。我们可以将要执行的SQL语句封装成函数,作为入参传入并执行。

    ​ 2、采用动态代理对执行SQL的前后做增强。

    编程式事务

    ​ Spring中采用函数式编程实现的事务,被称为编程式事务。编程式事务的实现相对简单,主要由类TransactionTemplate负责实现。具体代码可以见如下所示:

    @Override
    @Nullable
    public  T execute(TransactionCallback action) throws TransactionException {
       Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
    
       if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
          return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
       }
       else {
           //获取事务 
          TransactionStatus status = this.transactionManager.getTransaction(this);
          T result;
          try {
              //执行SQL语句内容
             result = action.doInTransaction(status);
          }
          catch (RuntimeException | Error ex) {
             //异常回滚
             rollbackOnException(status, ex);
             throw ex;
          }
          catch (Throwable ex) {
             // 异常回滚
             rollbackOnException(status, ex);
             throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
          }
           //提交事务
          this.transactionManager.commit(status);
          return result;
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    ​ TransactionCallBack作为入参传入,其中就主要是我们要执行的SQL语句内容。而其余部分可以看到,其实就和我们前面所描述的四步基本相似:

    1. 获取Mysql链接
    2. 执行SQL语句
    3. 提交SQL事务
    4. 存在异常则做Mysql的事务回滚。

    声明式事务

    ​ 在Spring中,采用AOP做增强逻辑的被称为声明式事务。相比起编程式事务,声明式事务相对复杂。因此,在了解声明式事务之前,我们需要先简单了解一下Spring是如何支持AOP(动态代理)。首先我们知道,Spring中Bean的存在形式有以下几个阶段:

    ​ 其中非常关键点就在BeanFactory。当我们对一个Bean定义代理对象后,BeanFactory生成的就不会是单纯的Bean实例对象,而是Bean的动态代理。通过调用Bean的动态代理中的方法,来实现AOP。那么如何自定义自己的AOP呢?要实现AOP需要明确两个点:

    1、需要在哪里做增强?(定义切点)

    2、需要做什么样的增强逻辑?(定义增强逻辑)

    ​ 对于这两点,Spring主要通过**事务代理管理配置类(ProxyTransactionManagementConfiguration)**进行实现。

    ​ 从类图中可以看到,事务代理管理配置类主要定义了三个Bean对象:

    • 注释事务属性源(AnnotationTransactionAttributeSource),其主要负责判断当前类是否为需要增强的类,即"哪里需要做增强"。
    • 事务拦截器(TransactionInterceptor),该类主要负责对事务做链接获取、事务提交以及事务回滚。即"怎么做增强"。
    • Bean工厂事务属性源指导(BeanFactoryTransactionAttributeSourceAdvisor),这个与事务本身无关,主要是在Bean工厂生产Bean实例的时候,方便对Bean进行替换使用的。其中主要是负责将定义的切点和增强逻辑注入到Spring中。

    这里我们逐一来介绍这三个Bean对象。

    注释事务属性源

    ​ “哪里需要做增强”,意味着类要具备判断是否需增强的能力。为此,注释事务属性源提供了一个关键的方法:isCandidateClass()

    但声明事务的注解一定不只一种。如果需要识别所有包下的事务型注解,一定会需要多次判断。因此,在注解事务属性源中,还保存了一组接口对象事务注释解析器(TransactionAnnotationParser),通过循环遍历这组事务注释解析器,就可以对不同框架注解进行处理。具体源码如下:

    @Override
    public boolean isCandidateClass(Class targetClass) {
       for (TransactionAnnotationParser parser : this.annotationParsers) {
          if (parser.isCandidateClass(targetClass)) {
             return true;
          }
       }
       return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ​ 以SpringTransactionAnnotationParser注释解析器为例,其实现的isCandidateClass()方法判断类是否被@Transactional类注释了,如果是,那么该类就是潜在的候选类。

    @Override
    public boolean isCandidateClass(Class targetClass) {
       return AnnotationUtils.isCandidateClass(targetClass, Transactional.class);
    }
    
    • 1
    • 2
    • 3
    • 4

    ​ 依次类推,对@TransactionAttribute等其他框架的注释,我们都可以采用这样方法实现。

    事务拦截器

    ​ 具备了判断哪些类需要执行事务的能力后,我们还需要确定具体的增强逻辑是什么样子的。而这就是事务拦截器主要功能。要实现这个功能,需要在对应方法被调用时,执行增强方法。

    ​ 从类图首先可以看到,为了能够察觉到方法的调用,事务拦截器实现了方法拦截器接口(MethodInterceptor)的invoke方法,在invoke方法中先判断当前执行的方法属于哪个类,紧接着会用invokeWithinTransaction()对方法进行事务性的包装。其源码如下:

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 判断执行的方法属于哪个类
       Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
        //再调用事务进行执行
       return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
          @Override
          @Nullable
          public Object proceedWithInvocation() throws Throwable {
             return invocation.proceed();
          }
          @Override
          public Object getTarget() {
             return invocation.getThis();
          }
          @Override
          public Object[] getArguments() {
             return invocation.getArguments();
          }
       });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ​ 主要逻辑放在invokeWithinTransaction()方法中。在该方法中,主要考虑了三类不同的编程方式的事务,分别是:响应式事务(ReactiveTransactionManager)回调优先型事务(CallbackPreferringPlatformTransactionManager)非回调优先型事务(非CallbackPreferringPlatformTransactionManager)

    三者的差异主要在于:

    1、响应式编程常采用Mono或Flux实现,需要对两种方式选择相应适配器做适配。

    2、后两者从名字上可以看出差异,回调型优先的事务,会先执行回调再执行事务。而非回调优先型事务,则关注于事务的执行,至于回调的失败与否不需要影响事务的回滚。

    尽管三者存在一些差异,但他们对于事务的实现其实是相似的,这里以非回调优先型事务为例子:

    @Nullable
    protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass,
          final InvocationCallback invocation) throws Throwable {
       TransactionAttributeSource tas = getTransactionAttributeSource();
       final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
       final TransactionManager tm = determineTransactionManager(txAttr);
    	.......
    
       PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
       final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
       if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
          // 创建事务
          TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
          Object retVal;
          try {
             // 执行方法
             retVal = invocation.proceedWithInvocation();
          } catch (Throwable ex) {
             // 回滚处理 + 抛出异常终止执行
             completeTransactionAfterThrowing(txInfo, ex);
             throw ex;
          } finally {
             cleanupTransactionInfo(txInfo);
          }
    		// 正常执行了事务,此时再执行回调
          if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
             TransactionStatus status = txInfo.getTransactionStatus();
             if (status != null && txAttr != null) {
                retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
             }
          }
    		// 提交事务
          commitTransactionAfterReturning(txInfo);
          return retVal;
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    ​ 源码本身不复杂,可以看到也是四步:

    1. 获取Mysql链接信息
    2. 执行SQL语句
    3. 提交SQL事务
    4. 存在异常则做Mysql的事务回滚。
    Bean工厂事务属性源指导

    ​ 对于Bean工厂事务属性源指导,其主要负责用于定义切点和增强逻辑,并将这些事务的逻辑注册到Spring中用于实现。如下是Bean工厂事务属性源指导的类图。

    ​ 从类图上可以看到,其继承了AbstractPointcutAdvisor关键模版类,该类是Spring中用于定义切点和增强逻辑。通过指定PointCut和Advice,就可以实现自定义的增强逻辑。因此,Bean工厂事务属性源指导只要将事务拦截器标记为增强逻辑,将注释事务属性源标记为切点,就可以让其在Spring中作为AOP生效。

    ​ 通过这三者的合作:注释事务属性源标注了切点(说明我那些方法需要做增强);事务拦截器定义了要执行的增强逻辑(说明我对这些方法怎么做增强);Bean工厂事务属性源指导则将切点和增强逻辑注入到Spring中使其生效。从而实现了Spring的声明式事务的内容。

    事务多样性支持

    ​ 在前述内容中,我们思考了SQL情况下如何实现事务。但有个问题,如果数据源换成Redission、换成分布式事务的API,代码还能快速复用么?简而言之,Spring是如何支持数据源多样性?如何确保新数据源的快速接入?

    ​ 对实现事务的流程做进一步抽象,不难发现一次事务中,框架需要关注的功能其实只有三个:

    1. 获取事务链接
    2. 提交事务
    3. 事务回滚

    ​ 因此,对不同的数据源,都可以将其抽象成这三个能力。应用层只需要对这三个能力进行调用,就不会在因为下层数据源的差异而需要大幅度的改动。而这正与面向接口设计的思想不谋而合

    ​ 为此,Spring专门设计了接口PlatformTransactionManager,其主要负责对外提供三个方法:getTransaction(definition)、commit(status)、rollback(status)。就用来抽象的上述的三个功能。由此一来,应用层的代码实现类(这里以TransactionTemplate为例子)就不再需要依赖于我的数据源究竟是JDBC、Redission还是DataSource。面对抽象编程,从而减少了接入需要考虑不同类型所带来的成本。

    总结

    ​ 本文介绍了Spring中针对SQL事务实现的两种方式:编程式事务声明式事务。同时介绍了对于多种不同的数据源,Spring在设计上的架构实现,希望对大家后续的开发设计有所帮助。

  • 相关阅读:
    深度学习 opencv python 实现中国交通标志识别 计算机竞赛_1
    IDEA中字符串怎么自动转义,双引号自动转义的小技巧
    JVM调优-JProfiler
    一起Talk Android吧(第四百二十五回:字节数组与String相互转换)
    什么是IOC和什么是AOP
    [附源码]Python计算机毕业设计Django动物保护协会网站
    【Linux】服务器部署:阿里云服务器购买配置与报价参考
    Pause Giant AI Experiments: An Open Letter(暂停大型人工智能实验: 一封公开信)
    Redis 集群详解及搭建过程
    大黑书《离散数学及其应用》之Dijkstra算法
  • 原文地址:https://blog.csdn.net/Huangjiazhen711/article/details/127766871