zoukankan      html  css  js  c++  java
  • Mybatis 源码分析之事物管理

    Mybatis 提供了事物的顶层接口:

    public interface Transaction {
    
      /**
       * Retrieve inner database connection
       * @return DataBase connection
       * @throws SQLException
       */
      Connection getConnection() throws SQLException;
    
      /**
       * Commit inner database connection.
       * @throws SQLException
       */
      void commit() throws SQLException;
    
      /**
       * Rollback inner database connection.
       * @throws SQLException
       */
      void rollback() throws SQLException;
    
      /**
       * Close inner database connection.
       * @throws SQLException
       */
      void close() throws SQLException;
    
    }

    还有一个事物工厂:

    public interface TransactionFactory {
    
      /**
       * Sets transaction factory custom properties.
       * @param props
       */
      void setProperties(Properties props);
    
      /**
       * Creates a {@link Transaction} out of an existing connection.
       * @param conn Existing database connection
       * @return Transaction
       * @since 3.1.0
       */
      Transaction newTransaction(Connection conn);
    
      /**
       * Creates a {@link Transaction} out of a datasource.
       * @param dataSource DataSource to take the connection from
       * @param level Desired isolation level
       * @param autoCommit Desired autocommit
       * @return Transaction
       * @since 3.1.0
       */
      Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
    
    }

    对于这两个接口我们一般是不直接操作的,但是它的影响是实实在在的。毕竟作为一个 ORM 框架,事物的管理是少不了的。它的实现大致可以分为两类,非 Spring 相关的事物和基于 Spring 管理的事物。

    非 Spring 相关

    关于 JdbcTransaction 和 ManagedTransaction 这两个实现就不多说了,实际上 getConnection 和 close 方法都是直接操作的 Connection。ManagedTransaction 的提交和回滚是个空的实现,交给容器了。

    Mybatis对外提供的统一接口是SqlSession,通常情况下我们可以这样使用:

    public void doSomethingWithTemplate(){
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        try {
            sqlSession = sqlSessionFactory.openSession();
            doSomething();
            sqlSession.commit();
        } catch (Exception e) {
            e.printStackTrace();
            sqlSession.rollback();
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
    }

    DefaultSqlSession 持有 Executor,将事物相关的操作做了简单的封装,Executor 又在此基础上加入了一级缓存等相关操作,如 commit 方法:

    public void commit(boolean required) throws SQLException {
        if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
        clearLocalCache();
        flushStatements();
        if (required) {
           transaction.commit();
        }
    }

    最终都调用了Mybatis提供的事物接口相关方法,以 JdbcTransaction 为例:

    public void commit() throws SQLException {
        if (connection != null && !connection.getAutoCommit()) {
          if (log.isDebugEnabled()) {
            log.debug("Committing JDBC Connection [" + connection + "]");
          }
          connection.commit();
        }
    }

    这个提交就是判断连接不为 null,而且不是自动提交的事物,那么就调用 JDBC 连接的 commit 方法,回滚也是类似。

    结合 Spring

    在mybatis-spring中,提供了一个实现:

    public class SpringManagedTransactionFactory implements TransactionFactory {
    
      public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new SpringManagedTransaction(dataSource);
      }
    
      public Transaction newTransaction(Connection conn) {
        throw new UnsupportedOperationException("New Spring transactions require a DataSource");
      }
    
      public void setProperties(Properties props) {
        // not needed in this version
      }
    
    }

    在SqlSessionFactoryBean 中的 buildSqlSessionFactory 方法中指定了事物工厂为 SpringManagedTransactionFactory,然后将其放到 Environment 实例中:

    if (this.transactionFactory == null) {
        this.transactionFactory = new SpringManagedTransactionFactory();
    }
    
    Environment environment = new Environment(this.environment, this.transactionFactory, this.dataSource);
    configuration.setEnvironment(environment);
    
    //......
    
    return this.sqlSessionFactoryBuilder.build(configuration);

    最后返回了sqlSessionFactoryBuilder 的 build 方法构建的 SqlSessionFactory:

    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }

    DefaultSqlSessionFactory#openSessionFromDataSource:

    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          final Environment environment = configuration.getEnvironment();
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
          final Executor executor = configuration.newExecutor(tx, execType);
          return new DefaultSqlSession(configuration, executor);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
    }

    这时候 DataSource 是 Spring 管理的,事物是 SpringManagedTransaction。连接是找 Spring 要的,跟踪一下发现事物由 Spring 直接处理了。

    管理与操作

    不管有没有 Spring,Mybatis 都要作为一个独立的 ORM 框架存在,所以事物管理是免不了的。Mybatis 定义了 Transaction 接口,对外提供的 SqlSession 间接操作了事物相关的接口。底层可以有不同的实现,处理起来更灵活。

    JdbcTransaction 中的实现就是直接操作 JDBC Connection,ManagedTransaction 的实现就是为空,不做任何处理。但是作为和 Spring 结合使用的 SpringManagedTransaction,这个就有点复杂了。

    都交给了 Spring,那么它怎么办,Mybatis 底层也有依赖于事物的操作,如缓存。

    SqlSessionUtils 中的 SqlSessionSynchronization 内部类继承了 TransactionSynchronizationAdapter,当 Spring 提交或者回滚时就会通过这个来回调。具体可以在事物提交前和提交后做一些操作,来弥补事物不在 Mybatis 这一方带来的缺憾。

    谁来管理,谁来操作

    看一下 SpringManagedTransaction 是怎么获取连接的:

    private void openConnection() throws SQLException {
        this.connection = DataSourceUtils.getConnection(this.dataSource);
        this.autoCommit = this.connection.getAutoCommit();
        this.isConnectionTransactional = isConnectionTransactional(this.connection, this.dataSource);
    
        if (this.logger.isDebugEnabled()) {
          this.logger.debug(
              "JDBC Connection ["
                  + this.connection
                  + "] will"
                  + (this.isConnectionTransactional ? " " : " not ")
                  + "be managed by Spring");
        }
    }

    再看下提交和回滚的实现:

    public void commit() throws SQLException {
        if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
          if (this.logger.isDebugEnabled()) {
            this.logger.debug("Committing JDBC Connection [" + this.connection + "]");
          }
          this.connection.commit();
        }
    }
    
    
    public void rollback() throws SQLException {
        if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
          if (this.logger.isDebugEnabled()) {
            this.logger.debug("Rolling back JDBC Connection [" + this.connection + "]");
          }
          this.connection.rollback();
        }
    }

    最后是否提交和回滚还得依赖一系列判断,其中 isConnectionTransactional 是这样判断的:

    public static boolean isConnectionTransactional(Connection con, DataSource dataSource) {
        if (dataSource == null) {
            return false;
        }
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        return (conHolder != null && connectionEquals(conHolder, con));
    }

    就是当前的JDBC连接是否是事务性的。如果是 Spring 管理的事物,这里就返回 true。如果不是,还能留一手,在这里补上一脚。

    状态的维护

    既然事物是 Spring 管理,它如何管理呢?

    就拿基于 AOP 的事物管理来说,切面都在 Service 层,考虑这样一种情况,一个线程中可能同时存在多个事物:把一个基于 Servlet 的请求看作一个线程,调用了一个 Service ,而这个 Service 包含了多个相关 Mapper 的操作,这些操作必须存在于同一个事物中。

    Mapper 方法的执行模板位于 SqlSessionTemplate:

    private class SqlSessionInterceptor implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          final SqlSession sqlSession = getSqlSession(
              SqlSessionTemplate.this.sqlSessionFactory,
              SqlSessionTemplate.this.executorType,
              SqlSessionTemplate.this.exceptionTranslator);
          try {
            Object result = method.invoke(sqlSession, args);
            if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
              // force commit even on non-dirty sessions because some databases require
              // a commit/rollback before calling close()
              sqlSession.commit(true);
            }
            return result;
          } catch (Throwable t) {
            Throwable unwrapped = unwrapThrowable(t);
            if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
              Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
              if (translated != null) {
                unwrapped = translated;
              }
            }
            throw unwrapped;
          } finally {
            closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          }
        }
    }

    这个和我们客户端的方法执行模板还是有区别的,第一个需要考虑的问题是:这个方法往上层追溯,必然是位于 Spring 的事物执行模板之中。那么这里的 SqlSession 的获取和关闭就不能随随便便,必须跟着“上面”走。

    查看源码可以看到,不论是 SqlSession 的获取还是关闭,都是基于当前事物的状态判断,而不是直接在 ThreadLocal 中拿或者直接创建一个新的,体现在代码中就是:

    SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);
    
    if (holder != null && holder.isSynchronizedWithTransaction()) {
       if (holder.getExecutorType() != executorType) {
           throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
       }
    
       holder.requested();
    
       if (logger.isDebugEnabled()) {
           logger.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
       }
    
       return holder.getSqlSession();
    }
    
    if (logger.isDebugEnabled()) {
       logger.debug("Creating a new SqlSession");
    }
    
    SqlSession session = sessionFactory.openSession(executorType);

    第一个就是 holder 不为 null, 第二是要求在同一个事物中,这些判断依赖于 ResourceHolderSupport 和 TransactionSynchronizationManager,这是比线程更细粒度的控制,来自 Spring 管理的事物状态。

    试想一下,作为底层的框架,事物由 Spring 管理,Mybatis 如何知道事物是否开启,如何判断是否在同一个事物,而这些它不能不知道,毕竟 SqlSession 是它管理的,而 SqlSession 的生命周期又和事物息息相关。

    上面举了一个例子,如果多个 Mapper 存在于同一个事物中,那么每次获取的 SqlSession 必然是同一个,不会创建新的,这样一级缓存也会发挥出功效。

    如果多个请求最终调用同一个 Mapper 呢?这时候 Mapper 持有的 SqlSession 是同一个(SqlSessionTemplate),但实际在 invoke 方法中获取 SqlSession 却各不相同。

    最后对 Mybatis 事物做一个总结:

    image

  • 相关阅读:
    pug 基础篇
    胡里胡哨-老师改卷纸
    javaScript 原生技巧
    angular9的学习(十二)插槽
    typescript高级编程(二)
    typescript高级编程(一)
    ActiveMq 使用指北
    基于ZooKeeper的分布式锁实现
    windows下配置启动脚本并设置开机自启及相应关闭脚本
    腾讯位置服务JavaScript API GL实现文本标记的碰撞避让
  • 原文地址:https://www.cnblogs.com/lucare/p/9312610.html
Copyright © 2011-2022 走看看