1. 程式人生 > 其它 >mybatis中的事務管理

mybatis中的事務管理

mybatis中的事務控制

mybatis中執行sql是從SqlSession開始的,SqlSession中提供了各種操作資料庫的方法

SqlSession中持有執行器Executor物件,通過執行器來執行sql

mybatis事務的本質是通過connection實現的,通過connection控制事務的提交,回滾,只有通過同一個connection執行的sql才能被控制住

一、mybatis單獨使用的情況

public class Test02 {

    public static void main(String[] args) throws IOException {
        InputStream resource = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resource);
        //獲取sqlsession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //通過sqlsession執行sql
        sqlSession.insert("com.lyy.mybatis_source.mapper.BankMapper.insert");
        //提交事務
        sqlSession.commit();
    }
}

單獨使用mybatis時,一般會按上邊的步驟來進行。其中openSession方法如果傳true創建出的sqlsession會自動提交事務,傳false或者不傳得到的sqlsession需要手動呼叫commit方法來提交事務

SqlSessionFactory是一個介面,SqlSessionFactoryBuilder.build方法得到的是其實現類DefaultSqlSessionFactory的物件,openSession方法會得到DefaultSqlSession物件,下面來分析下

DefaultSqlSession的commit方法,

@Override
  public void commit() {
    commit(false);
  }

  @Override
  public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

可以看到這個commit方法最終會呼叫執行器executor的commit方法.執行器Executor是一個介面,常用的實現類有三個SimpleExecutor,ReuseExecutor,BatchExecutor,

sqlSessionFactory.openSession方法呼叫的時候可以指定執行器的型別,如果不指定建立的是SimpleExecutor

所以繼續分析這個執行器的commit方法,其呼叫的是父類BaseExecutor中的方法

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

可以看到最後呼叫的是transaction.commit(),這個transaction是BaseExecutor類中的一個屬性

protected Transaction transaction;

Transaction是mybatis定義的一個介面,其中提供了獲取連線,操作事務等的方法,

public interface Transaction {

  Connection getConnection() throws SQLException;

  void commit() throws SQLException;

  void rollback() throws SQLException;

  void close() throws SQLException;

  Integer getTimeout() throws SQLException;

}

執行時使用哪個實現類取決於

mybatis配置檔案中指定的transactionManager型別

<environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/transaction_test"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
</environments>

這個配置檔案中指定的是JDBC,所以最後會使用JdbcTransaction這個實現類

繼續看這個類中的commit方法

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

其中connection是這個類中的一個屬性,代表的是執行sql時使用的資料庫連線,這裡通過連線物件執行commit方法來提交事務,並且如果這個連線設定的是自動提交事務這個方法就什麼都不做。

總結下來就是sqlSession.commit方法最終會通過Transaction中的connection來提交事務。

sqlsession--->Executor-->Transaction-->connection

可以推斷,sqlsession中執行sql時肯定也是呼叫這個Transaction.getConnection來獲取連線,這樣才能保證執行sql時和提交事務時使用的是同一個連線

JdbcTransaction中獲取連線的方法

  public Connection getConnection() throws SQLException {
    if (connection == null) {
      openConnection();
    }
    return connection;
  }

再來思考一個問題,Executor中的Transaction是什麼時候賦值的?

這就需要看下DefaultSqlSessionFactory.openSession()方法,

  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // configuration是DefaultSqlSessionFactory中的一個屬性
      final Environment environment = configuration.getEnvironment();
      // 解析配置檔案的過程中會創建出transactionFactory,並設定給environment
      // 再把environment設定到configuration物件的屬性上
        final TransactionFactory transactionFactory = 
          getTransactionFactoryFromEnvironment(environment);
      // 通過transactionFactory來獲取Transaction物件
      // 基於我們的配置檔案這裡使用的是JdbcTransactionFactory
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //建立 Executor物件時傳入了tx物件和執行器型別
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } 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();
    }
  }

我們mybatis配置檔案中配置的事務型別是JDBC,所以這裡使用的TransactionFactory是 JdbcTransactionFactory

public class JdbcTransactionFactory implements TransactionFactory {

  @Override
  public Transaction newTransaction(Connection conn) {
    return new JdbcTransaction(conn);
  }

  @Override
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    return new JdbcTransaction(ds, level, autoCommit);
  }
}

所以當我們使用同一個sqlSession來執行多條sql時,因為每次都是按

sqlsession--->Executor-->Transaction-->connection 這樣的方式去獲取資料庫連線,所以第一次執行時會獲取一個新連線,後續的執行都是從Transaction中拿到已有的connection執行sql,保證了使用同一個connection,

最終再通過這個connection來提交事務

二、mybatis和spring整合的情況

mybatis官方提供了一個mybatis-spring包可以用來整合spring。

整合spring後,最終的sql語句還是要通過SqlSession來執行的,也就是DefaultSqlSession,只不過為了和spring整合做了幾層代理,所以從容器中獲取的sqlSession是SqlSessionTemplate型別的,這個類也實現了

SqlSession介面

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    
    // 這是sqlsession的代理
    private final SqlSession sqlSessionProxy;
    
    //這是構造方法
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    
    //這是在構造方法中給sqlsession的代理賦值
    //具體邏輯是在SqlSessionInterceptor中實現,它是當前類中的內部類
    this.sqlSessionProxy = 
        (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class }, new SqlSessionInterceptor());
  }
  
  //內部類
  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在這個invoke方法中才會真正的去獲取Sqlession,然後用sqlsession去執行sql
        //這裡獲取到的肯定也是 DefaultSqlSession
        SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
        try {
            //執行方法,可以看做是對上邊獲取到的sqlSession物件中的方法用動態代理來做增強
            Object result = method.invoke(sqlSession, args);
            //這些就是增強邏輯
            //這個判斷的意思是如果沒有用spring來控制事務,就在這裡使用sqlSession提交事務
            //如果spring控制事務,這裡不做操作讓spring統一管理事務
            if (!isSqlSessionTransactional(sqlSession, 	
                                           SqlSessionTemplate.this.sqlSessionFactory)) {
              sqlSession.commit(true);
            }
        return result;
        } catch {
            //省略
        } finally {
            //省略
        }
    }
  }
}

這個 getSqlSession方法是mybatis-spring包中提供的工具類SqlSessionUtils中的方法

SqlSessionUtils

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
	// TransactionSynchronizationManager是spring提供的一個同步器,裡邊有許多ThreadLocal,
    // 通過它可以保證同一個執行緒範圍內多次獲取得到的是同一個sqlsession
    // 它裡邊以鍵值對的形式存資料,在這裡sessionFactory就是鍵,
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    LOGGER.debug(() -> "Creating a new SqlSession");
    //第一次訪問時需要新開啟一個Sqlsession然後註冊到同步器中,下次訪問就可以直接獲取
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

上邊的程式碼保證了呼叫dao方法執行sql時,同一個執行緒範圍內使用的都是同一個sqlsession物件,

再根據第一部分的結論 sqlsession--->Executor-->Transaction 就可以保證每次獲取的都是同一個transaction。

那麼思考一個問題,spring怎麼來控制這種情況的事務?

分析下,要控制事務,spring必須獲取到connection,而從上邊的分析connection被封裝在Transaction中,spring如何獲取到connection呢

mybatis-spring中也提供了一個Transaction的實現類,SpringManagedTransaction

這種情況下使用的是這個實現類,看下其中獲取connection的方法

SpringManagedTransaction中的原始碼

public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
  }

同樣的,只有第一次呼叫會新建,其餘直接返回。繼續看DataSourceUtils.getConnection

這是spring-jdbc提供的一個獲取連線的工具類,也就是mybatis是通過spring來獲取connect,這樣就有機會把這個connection再暴露給spring,後邊spring就可以通過這個connection來管理事務

public abstract class DataSourceUtils {
    
    public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
        try {
            //呼叫這個方法去真正獲取連線
            return doGetConnection(dataSource);
        } catch (SQLException var2) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", var2);
        } catch (IllegalStateException var3) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + var3.getMessage());
        }
    }

    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");
        //可以看到是先從同步器TransactionSynchronizationManager中獲取連線
        // 這裡邊有ThreadLocal可以存資料庫連線,第一次獲取建立一個connection存到同步器裡
        // 下次再訪問時就可以從同步器中直接取。
        ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
            logger.debug("Fetching JDBC Connection from DataSource");
            //進到這裡表示是第一次獲取連線,這個方法中會從dataSource獲取連線
            Connection con = fetchConnection(dataSource);
            if (TransactionSynchronizationManager.isSynchronizationActive()) {
                try {
                    ConnectionHolder holderToUse = conHolder;
                    if (conHolder == null) {
                        holderToUse = new ConnectionHolder(con);
                    } else {
                        conHolder.setConnection(con);
                    }

                    holderToUse.requested();
                    //註冊connection到同步器中,下次可以直接獲取
                    TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
                    holderToUse.setSynchronizedWithTransaction(true);
                    if (holderToUse != conHolder) {
                        TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                    }
                } catch (RuntimeException var4) {
                    releaseConnection(con, dataSource);
                    throw var4;
                }
            }

            return con;
        } else {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }

            return conHolder.getConnection();
        }
    }
    
}

總結下來這個方法會先從同步器TransactionSynchronizationManager中獲取連線,如果獲取不到就新建一個再放進去,而這個類是spring提供的,所以spring事務管理模組可以通過這個類來獲取連線。

所以總結下spring+mybatis的事務管理流程,對於一個有事務的service方法,

(1) 剛開始肯定是spring先開啟事務,開啟事務就是獲取連線,設定連線的autoCommit為false,然後會把連線跟當前執行緒繫結,設定到同步器TransactionSynchronizationManager

(2) mybatis執行sql時按照

sqlsession--->Executor-->Transaction-->DataSourceUtils-->TransactionSynchronizationManager

-->connection 就可以獲取到spring開啟事務時放進去的那個connection,然後執行sql

(3) 不管中間執行了多少sql,因為同一個執行緒內使用的是同一個sqlSesssion,所以

都是通過同一個connection執行的

(4) spring從TransactionSynchronizationManager中獲取到connection提交或回滾事務

在補充兩點,上邊講到spring+mybaits時執行sql使用的是SqlSessionTemplate,因為它實現了Sqlsession介面,所以其中也有commit方法,但直接調這個方法會拋異常,官方不讓這樣呼叫

  @Override
  public void commit() {
    throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
  }

那如果我整合了spring後直接通過DefaultSqlSession來呼叫commit方法會怎麼樣呢?

我們思考下這個commit方法最終調的是Transaction中的commit,此時實現類是SpringManagedTransaction

看下其原始碼

  @Override
  public void commit() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      LOGGER.debug(() -> "Committing JDBC Connection [" + this.connection + "]");
      this.connection.commit();
    }
  }

注意這裡頭有個isConnectionTransactional變數,當spring事務開啟時,走到這個方法時這個變數會是true,所以不進if,這個方法什麼都不做。還是會讓spring來管理事務,調sqlsession.commit方法是無效的。

至於spring是如何進行事務管理的,這又是一大內容,後邊再具體描述。簡單來講,spring的事務管理是基於aop

實現的,方法執行時實現事務功能的入口在TransactionInterceptor這個類的invoke方法中,

然後會執行到TransactionAspectSupport#createTransactionIfNecessary-->

AbstractPlatformTransactionManager#getTransaction-->startTransaction