從源碼的角度解析Mybatis的會話機制

微信公衆號「後端進階」,專一後端技術分享:Java、Golang、WEB框架、分佈式中間件、服務治理等等。
老司機傾囊相授,帶你一路進階,來不及解釋了快上車!

坐在我旁邊的鐘同窗據說我精通Mybatis源碼(我就想不通,是誰透漏了風聲),就順帶問了我一個問題:在同一個方法中,Mybatis屢次請求數據庫,是否要建立多個SqlSession會話?java

可能最近擼多了,當時腦子裏一片模糊,眼神迷離,雖然我當時回答他:若是多個請求同一個事務中,那麼多個請求都在共用一個SqlSession,反之每一個請求都會建立一個SqlSession。這是咱們在日常開發中都習覺得常的常識了,但我卻沒有從原理的角度給鍾同窗分析,致使鍾同窗茶飯不思,做爲老司機的我,感到深深的自責,因而我暗自下定決心,要給鍾同窗一個交代。git

不服跑個demo

測試在方法中不加事務時,每一個請求是否會建立一個SqlSession:github

從日誌能夠看出,在沒有加事務的狀況下,確實是Mapper的每次請求數據庫,都會建立一個SqlSession與數據庫交互,下面咱們再看看加了事務的狀況:spring

從日誌能夠看出,在方法中加了事務後,兩次請求只建立了一個SqlSession,再次證實了我上面的回答,可是僅僅這樣回答是體現徹底不出一個老司機應有的職業素養的,因此,我要發車了。sql

什麼是SqlSession

在發車以前,咱們必須得先搞明白,什麼是SqlSession?數據庫

簡單來講,SqlSession是Mybatis工做的最頂層API會話接口,全部的數據庫操做都經由它來實現,因爲它就是一個會話,即一個SqlSession應該僅存活於一個業務請求中,也能夠說一個SqlSession對應這一次數據庫會話,它不是永久存活的,每次訪問數據庫時都須要建立它。後端

所以,SqlSession並非線程安全,每一個線程都應該有它本身的 SqlSession 實例,千萬不能將一個SqlSession搞成單例形式,或者靜態域和實例變量的形式都會致使SqlSession出現事務問題,這也就是爲何多個請求同一個事務中會共用一個SqlSession會話的緣由,咱們從SqlSession的建立過程來講明這點:安全

  1. 從Configuration配置類中拿到Environment數據源;
  2. 從數據源中獲取TransactionFactory和DataSource,並建立一個Transaction鏈接管理對象;
  3. 建立Executor對象(SqlSession只是全部操做的門面,真正要幹活的是Executor,它封裝了底層JDBC全部的操做細節);
  4. 建立SqlSession會話。

每次建立一個SqlSession會話,都會伴隨建立一個專屬SqlSession的鏈接管理對象,若是SqlSession共享,就會出現事務問題。微信

從源碼的角度分析

源碼分析從哪一步做爲入口呢?若是是看過我以前寫的那幾篇關於mybatis的源碼分析,我相信你不會在Mybatis源碼前磨磨蹭蹭,遲遲找不到入口。session

在以前的文章裏已經說過了,Mapper的實現類是一個代理,真正執行邏輯的是MapperProxy.invoke(),該方法最終執行的是sqlSessionTemplate。

org.mybatis.spring.SqlSessionTemplate:

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;
  this.sqlSessionProxy = (SqlSession) newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class },
    new SqlSessionInterceptor());
}

這個是建立SqlSessionTemplate的最終構造方法,能夠看出sqlSessionTemplate中用到了SqlSession,是SqlSessionInterceptor實現的一個動態代理類,因此咱們直接深刻要塞:

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    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) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        sqlSession = null;
        Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
        if (translated != null) {
          unwrapped = translated;
        }
      }
      throw unwrapped;
    } finally {
      if (sqlSession != null) {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

Mapper全部的方法,最終都會用這個方法來處理全部的數據庫操做,茶飯不思的鐘同窗眼神迷離不知道是否是自暴自棄致使擼多了,眼神空洞地望着我,問我spring整合mybatis和mybatis單獨使用是否有區別,其實沒區別,區別就是spring封裝了全部處理細節,你就不用寫大量的冗餘代碼,專一於業務開發。

該動態代理方法主要作了如下處理:

  1. 根據當前條件獲取一個SqlSession,此時SqlSession多是新建立的也有多是獲取到上一次請求的SqlSession;
  2. 反射執行SqlSession方法,再判斷當前會話是不是一個事務,若是是一個事務,則不commit;
  3. 若是此時拋出異常,判斷若是是PersistenceExceptionTranslator且不爲空,那麼就關閉當前會話,而且將sqlSession置爲空防止finally重複關閉,PersistenceExceptionTranslator是spring定義的數據訪問集成層的異常接口;
  4. finally不管怎麼執行結果如何,只要當前會話不爲空,那麼就會執行關閉當前會話操做,關閉當前會話操做又會根據當前會話是否有事務來決定會話是釋放仍是直接關閉

org.mybatis.spring.SqlSessionUtils#getSqlSession:

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

  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

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

  if (LOGGER.isDebugEnabled()) {
    LOGGER.debug("Creating a new SqlSession");
  }

  session = sessionFactory.openSession(executorType);

  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

  return session;
}

是否是看到了不服跑個demo時看到的日誌「Creating a new SqlSession」了,那麼證實我直接深刻的地方挺準確的,沒有絲毫偏差。在這個方法當中,首先是從TransactionSynchronizationManager(如下稱當前線程事務管理器)獲取當前線程threadLocal是否有SqlSessionHolder,若是有就從SqlSessionHolder取出當前SqlSession,若是當前線程threadLocal沒有SqlSessionHolder,就從sessionFactory中建立一個SqlSession,具體的建立步驟上面已經說過了,接着註冊會話到當前線程threadLocal中。

先來看看當前線程事務管理器的結構:

public abstract class TransactionSynchronizationManager {
  // ...
  // 存儲當前線程事務資源,好比Connection、session等
  private static final ThreadLocal<Map<Object, Object>> resources =
    new NamedThreadLocal<>("Transactional resources");
  // 存儲當前線程事務同步回調器
  // 當有事務,該字段會被初始化,即激活當前線程事務管理器
  private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    new NamedThreadLocal<>("Transaction synchronizations");
  // ...
}

這是spring的一個當前線程事務管理器,它容許將當前資源存儲到當前線程ThreadLocal中,從前面也可看出SqlSessionHolder是保存在resources中。

org.mybatis.spring.SqlSessionUtils#registerSessionHolder:

private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
                                          PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
  SqlSessionHolder holder;
  // 判斷當前是否有事務
  if (TransactionSynchronizationManager.isSynchronizationActive()) {
    Environment environment = sessionFactory.getConfiguration().getEnvironment();
    // 判斷當前環境配置的事務管理工廠是不是SpringManagedTransactionFactory(默認)
    if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]");
      }

      holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
      // 綁定當前SqlSessionHolder到線程ThreadLocal中
      TransactionSynchronizationManager.bindResource(sessionFactory, holder);
      // 註冊SqlSession同步回調器
      TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
      holder.setSynchronizedWithTransaction(true);
      // 會話使用次數+1
      holder.requested();
    } else {
      if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
        }
      } else {
        throw new TransientDataAccessResourceException(
          "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
      }
    }
  } else {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
    }
  }
}

註冊SqlSession到當前線程事務管理器的條件首先是當前環境中有事務,不然不註冊,判斷是否有事務的條件是synchronizations的ThreadLocal是否爲空:

public static boolean isSynchronizationActive() {
  return (synchronizations.get() != null);
}

每當咱們開啓一個事務,會調用initSynchronization()方法進行初始化synchronizations,以激活當前線程事務管理器。

public static void initSynchronization() throws IllegalStateException {
  if (isSynchronizationActive()) {
    throw new IllegalStateException("Cannot activate transaction synchronization - already active");
  }
  logger.trace("Initializing transaction synchronization");
  synchronizations.set(new LinkedHashSet<TransactionSynchronization>());
}

因此當前有事務時,會註冊SqlSession到當前線程ThreadLocal中。

Mybatis本身也實現了一個自定義的事務同步回調器SqlSessionSynchronization,在註冊SqlSession的同時,也會將SqlSessionSynchronization註冊到當前線程事務管理器中,它的做用是根據事務的完成狀態回調來處理線程資源,即當前若是有事務,那麼當每次狀態發生時就會回調事務同步器,具體細節可移步至Spring的org.springframework.transaction.support包。

回到SqlSessionInterceptor代理類的邏輯,發現判斷會話是否須要提交要調用如下方法:

org.mybatis.spring.SqlSessionUtils#isSqlSessionTransactional:

public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
  notNull(session, NO_SQL_SESSION_SPECIFIED);
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

  return (holder != null) && (holder.getSqlSession() == session);
}

取決於當前SqlSession是否爲空而且判斷當前SqlSession是否與ThreadLocal中的SqlSession相等,前面也分析了,若是當前沒有事務,SqlSession是不會保存到事務同步管理器的,即沒有事務,會話提交。

org.mybatis.spring.SqlSessionUtils#closeSqlSession:

public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
  notNull(session, NO_SQL_SESSION_SPECIFIED);
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
  if ((holder != null) && (holder.getSqlSession() == session)) {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Releasing transactional SqlSession [" + session + "]");
    }
    holder.released();
  } else {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Closing non transactional SqlSession [" + session + "]");
    }
    session.close();
  }
}

方法不管執行結果如何都須要執行關閉會話邏輯,這裏的判斷也是判斷當前是否有事務,若是SqlSession在事務當中,則減小引用次數,沒有真實關閉會話。若是當前會話不存在事務,則直接關閉會話。

寫在最後

雖然說鍾同窗問了我一個Mybatis的問題,我卻中了Spring的圈套,猛然發現整個事務鏈路都處在Spring的管控當中,這裏涉及到了Spring的自定義事務的一些機制,其中當前線程事務管理器是整個事務的核心與中軸,當前有事務時,會初始化當前線程事務管理器的synchronizations,即激活了當前線程同步管理器,當Mybatis訪問數據庫會首先從當前線程事務管理器獲取SqlSession,若是不存在就會建立一個會話,接着註冊會話到當前線程事務管理器中,若是當前有事務,則會話不關閉也不commit,Mybatis還自定義了一個TransactionSynchronization,用於事務每次狀態發生時回調處理。

鍾同窗,this is for you!

公衆號「後端進階」,專一後端技術分享!

相關文章
相關標籤/搜索