首頁 > Java > java教程 > 原始碼的角度解析Mybatis的會話機制(詳細)

原始碼的角度解析Mybatis的會話機制(詳細)

不言
發布: 2019-03-21 16:31:01
轉載
3091 人瀏覽過

這篇文章帶給大家的內容是關於源碼的角度解析Mybatis的會話機制(詳細),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

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

可能最近擼多了,當時腦子裡一片模糊,眼神迷離,雖然我當時回答他:如果多個請求同一個事務中,那麼多請求都在共用一個SqlSession,反之每個請求都會建立一個SqlSession。這是我們在平常開發中都習以為常的常識了,但我卻沒有從原理的角度給鍾同學分析,導致鍾同學茶飯不思,作為老司機的我,感到深深的自責,於是我暗自下定決心,要給鐘同學一個交代。

不服跑demo

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

原始碼的角度解析Mybatis的會話機制(詳細)

從日誌可以看出,在沒有加事務的情況下,確實是Mapper的每次請求資料庫,都會創建一個SqlSession與資料庫交互,下面我們再看看加了事務的情況:

原始碼的角度解析Mybatis的會話機制(詳細)

從日誌可以看出,在方法中加了事務後,兩次請求只創建了一個SqlSession,再次證明了我上面的回答,但是光是這樣回答是體現完全不出一個老司機應有的職業素養的,所以,我要發車了。

什麼是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源碼前磨磨蹭蹭,遲遲找不到入口。

在先前的文章裡已經說過了,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>> resources =
    new NamedThreadLocal("Transactional resources");
  // 存储当前线程事务同步回调器
  // 当有事务,该字段会被初始化,即激活当前线程事务管理器
  private static final ThreadLocal<set>> synchronizations =
    new NamedThreadLocal("Transaction synchronizations");
  // ...
}</set></map>
登入後複製

这是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>());
}</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,用于事务每次状态发生时回调处理。

本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的Java教程视频栏目!

以上是原始碼的角度解析Mybatis的會話機制(詳細)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:segmentfault.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板