1. 程式人生 > 其它 >mybatis-spring中的快取失效問題

mybatis-spring中的快取失效問題

技術標籤:學習總結

一. mybatis中的一級快取

首先我們來說一下mybatis中的一級快取。
在這裡插入圖片描述
mybatis中使用預設的SQLSession——DefaultSqlSession,每一個DefaultSqlSession中都持有一個Executor——BaseExecutorBaseExecutor中含有一個屬性localCachelocalCache便是mybatis中的一級快取。當用戶初次發起查詢時,會從資料庫查詢資料,並將查詢結果儲存在localCache中。

private <E> List<E> queryFromDatabase(MappedStatement ms,
Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 初次查詢從資料庫查詢資料 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql)
; } finally { localCache.removeObject(key); } // 將查詢結果儲存到一級快取中 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }

所以說,一個SqlSession對應一個一級快取

二.mybatis-spring中快取失效原始碼分析

我們先來說一下spring是如何整合mybatis的,用到的是spring中非常重要的一個介面——FactoryBean。mybatis-spring中建立了一個類MapperFactoryBean,它實現了FactoryBean介面,並通過getObject()方法返回Mapper物件

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }
  ...
}

前面說了,mybatis中的一級快取和SqlSession有關,所以我們要去找到這個SqlSession。而mybatis-spring中的SqlSession具體的實現類是SqlSessionTemplate。(大家可以自己一步步跟蹤原始碼看一下)

public class SqlSessionTemplate implements SqlSession, DisposableBean {
	/**
     * mybatis-spring中真正使用的SqlSession實現類
     */ 
	private final SqlSession sqlSessionProxy;

	@Override
	public void select(String statement, Object parameter, ResultHandler handler) {
	    this.sqlSessionProxy.select(statement, parameter, handler);
	}
	...
}

通過檢視SqlSessionTemplate類,我們可以得出以下兩點資訊。

  • 該類中還包含了一個SqlSession——sqlSessionProxy,這是一個代理類
  • 其中的select()方法使用的sqlSessionProxy

我們在mybatis-spring中的查詢使用的SqlSession,便是這個sqlSessionProxy,這也是mybatis-spring真正使用的SqlSession。

因為sqlSessionProxy是一個代理類,那它的方法是在InvocationHandler 介面中的invoke()方法中呼叫的。

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 獲取SqlSession
      // 如果該方法上加了事務,每次呼叫該方法的時候是同一個SqlSession
      // 否則,每次呼叫該方法的時候,都會建立一個新的SqlSession
      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)) {
          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) {
          // 關閉SqlSession
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

上面的方法中通過getSqlSession()方法獲取SqlSession

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");
    }

	// 新建立一個SqlSession
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

呼叫了TransactionSynchronizationManager.getResource(sessionFactory)方法去獲取SqlSessionHolder,這個resources屬性是一個ThreadLocal型別的,SqlSession儲存在ThreadLocal中,這意味著不同執行緒的SqlSession也是不同的,這避免了多執行緒環境下的執行緒安全問題。如果查詢方法沒有新增事務,那麼便不能從ThreadLocal中獲取到SqlSession,只有通過
session = sessionFactory.openSession(executorType);建立一個新的SqlSession

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");

因此,每次呼叫查詢方法的時候,獲取的SqlSession都是新建立的,所以實質上並不是mybatis的一級快取失效了,而是每次查詢所使用的的SqlSession不同。

總結:

  1. 如果方法上沒有新增事務,每一次查詢方法使用的都是新建立的SqlSession,其他SqlSession上的一級快取使用不到,所以也說成一級快取失效了。
  2. 如果方法上添加了事務,每一次查詢使用的都是同一個SqlSession,一級快取沒有失效。(單執行緒環境下)
  3. mybatis-spring的SqlSession儲存在ThreadLocal中,不同執行緒獲取到的SqlSession是不同的,這避免了多執行緒環境下的執行緒安全問題