試問Java中 同一個方法中Mybatis多次請求數據庫是否會創建多個會話

最近在開發過程中,小夥伴們有一個疑問:我們在寫一個服務層的方法需要多次請求mybatis的dao(即mybatis的Mapper)方法,那會不會因爲頻繁跟數據庫交互導致性能走低呢?
跟着疑問我們結合demo分析下mybatis的源碼,來一層層解開大家的疑惑

第一步:調用dao層的方法上不加事務@transactional
在這裏插入圖片描述
從日誌可以看出,在沒有加事務的情況下,確實是Mapper的每次請求數據庫,都會創建一個SqlSession與數據庫交互

第二步:我們再看看加了事務的情況:
在這裏插入圖片描述

從日誌可以看出,在方法中加了事務後,兩次請求只創建了一個SqlSession

看到以上兩個日誌對比圖,我們可能會問爲什麼加了事務就公用一個Sqlsession,好,別急,下面我們會根據源碼給大家一一解析

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> 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());
}
所以當前有事務時,會註冊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在事務當中,則減少引用次數,沒有真實關閉會話。如果當前會話不存在事務,則直接關閉會話。

最後總結
涉及到了Spring的自定義事務的一些機制,其中當前線程事務管理器是整個事務的核心與中軸,當前有事務時,會初始化當前線程事務管理器的synchronizations,即激活了當前線程同步管理器,當Mybatis訪問數據庫會首先從當前線程事務管理器獲取SqlSession,如果不存在就會創建一個會話,接着註冊會話到當前線程事務管理器中,如果當前有事務,則會話不關閉也不commit,Mybatis還自定義了一個TransactionSynchronization,用於事務每次狀態發生時回調處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章