mybatis-spring中的快取失效問題
技術標籤:學習總結
一. mybatis中的一級快取
首先我們來說一下mybatis
中的一級快取。
mybatis
中使用預設的SQLSession
——DefaultSqlSession
,每一個DefaultSqlSession
中都持有一個Executor
——BaseExecutor
,BaseExecutor
中含有一個屬性localCache
,localCache
便是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
不同。
總結:
- 如果方法上沒有新增事務,每一次查詢方法使用的都是新建立的SqlSession,其他SqlSession上的一級快取使用不到,所以也說成一級快取失效了。
- 如果方法上添加了事務,每一次查詢使用的都是同一個SqlSession,一級快取沒有失效。(單執行緒環境下)
- mybatis-spring的SqlSession儲存在ThreadLocal中,不同執行緒獲取到的SqlSession是不同的,這避免了多執行緒環境下的執行緒安全問題