分析下爲何spring 整合mybatis後爲啥用不上session緩存

看了@Elim的 Mybatis介紹之緩存java

中間有提到spring

須要注意的是當Mybatis整合Spring後,直接經過Spring注入Mapper的形式,若是不是在同一個事務中每一個Mapper的每次查詢操做都對應一個全新的SqlSession實例sql

由於一直用spring整合了mybatis,因此不多用到mybatis的session緩存。 習慣是本地緩存本身用map寫或者引入第三方的本地緩存框架ehcache,Guavaapache

 

因此提出來糾結下緩存

實驗下(spring整合mybatis略,網上一堆),先看看mybatis級別的session的緩存session

 

放出打印sql語句mybatis

configuration.xml 加入app

<settings>
		<!-- 打印查詢語句 -->
		<setting name="logImpl" value="STDOUT_LOGGING" />
	</settings>

 

測試源代碼以下:框架

dao類ide

/**
 * 測試spring裏的mybatis爲啥用不上緩存
 * 
 * @author 何錦彬 2017.02.15
 */
@Component
public class TestDao {

    private Logger logger = Logger.getLogger(TestDao.class.getName());

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    /**
     * 兩次SQL
     * 
     * @param id
     * @return
     */
    public TestDto selectBySpring(String id) {

        TestDto testDto = (TestDto) sqlSessionTemplate.selectOne("com.hejb.TestDto.selectByPrimaryKey", id);
        testDto = (TestDto) sqlSessionTemplate.selectOne("com.hejb.TestDto.selectByPrimaryKey", id);
        return testDto;
    }

    /**
     * 一次SQL
     * 
     * @param id
     * @return
     */
    public TestDto selectByMybatis(String id) {

        SqlSession session = sqlSessionFactory.openSession();
        TestDto testDto = session.selectOne("com.hejb.TestDto.selectByPrimaryKey", id);
        testDto = session.selectOne("com.hejb.TestDto.selectByPrimaryKey", id);
        return testDto;
    }

}

 

測試service類

@Component
public class TestService {
    @Autowired
    private TestDao testDao;

    /**
     * 未開啓事務的spring Mybatis查詢
     */
    public void testSpringCashe() {

        //查詢了兩次SQL
        testDao.selectBySpring("1");
    }

    /**
     * 開啓事務的spring Mybatis查詢
     */
    @Transactional
    public void testSpringCasheWithTran() {

        //spring開啓事務後,查詢1次SQL
        testDao.selectBySpring("1");
    }

    /**
     * mybatis查詢
     */
    public void testCash4Mybatise() {

        //原生態mybatis,查詢了1次SQL
        testDao.selectByMybatis("1");
    }

}


輸出結果:

testSpringCashe()方法執行了兩次SQL, 其它都是一次

 

源碼追蹤:

先看mybatis裏的sqlSession

跟蹤到最後 調用到 org.apache.ibatis.executor.BaseExecutor的query方法

try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; //先從緩存中取
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); //注意裏面的key是CacheKey
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }

貼下是怎麼取出緩存數據的代碼

private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
    if (ms.getStatementType() == StatementType.CALLABLE) {
      final Object cachedParameter = localOutputParameterCache.getObject(key);//從localOutputParameterCache取出緩存對象
      if (cachedParameter != null && parameter != null) {
        final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
        final MetaObject metaParameter = configuration.newMetaObject(parameter);
        for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
          if (parameterMapping.getMode() != ParameterMode.IN) {
            final String parameterName = parameterMapping.getProperty();
            final Object cachedValue = metaCachedParameter.getValue(parameterName);
            metaParameter.setValue(parameterName, cachedValue);
          }
        }
      }
    }
  }

 

發現就是從localOutputParameterCache就是一個PerpetualCache, PerpetualCache維護了個map,就是session的緩存本質了。

重點能夠關注下面兩個累的邏輯

PerpetualCache , 兩個參數, id和map

CacheKey,map中存的key,它有覆蓋equas方法,當獲取緩存時調用.

 

這種本地map緩存獲取對象的缺點,就我踩坑經驗(之前我也用map去實現的本地緩存),就是獲取的對象非clone的,返回的兩個對象都是一個地址

 

而在spring中通常都是用sqlSessionTemplate,以下

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:configuration.xml" />
		<property name="mapperLocations">
			<list>
				<value>classpath*:com/hejb/sqlmap/*.xml</value>
			</list>
		</property>
	</bean>
	<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg ref="sqlSessionFactory" />
	</bean>

 

在SqlSessionTemplate中執行SQL的session都是經過sqlSessionProxy來,sqlSessionProxy的生成在構造函數中賦值,以下:

this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());

sqlSessionProxy經過JDK的動態代理方法生成的一個代理類,主要邏輯在InvocationHandler對執行的方法進行了先後攔截,主要邏輯在invoke中,包好了每次執行對sqlsesstion的建立,common,關閉

代碼以下:

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 每次執行前都建立一個新的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)) {
          // 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);
        }
      }
    }
  }

由於每次都進行建立,因此就用不上sqlSession的緩存了. 

對於開啓了事務爲何能夠用上呢, 跟入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);

     // 首先從SqlSessionHolder裏取出session
    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;
  }

 

在裏面維護了個SqlSessionHolder,關聯了事務與session,若是存在則直接取出,不然則新建個session,因此在有事務的裏,每一個session都是同一個,故能用上緩存了

 

留下個下小思考

,CacheKey是怎麼做爲KEY來判斷是否執行的是同一條SQL與參數的呢

相關文章
相關標籤/搜索