本篇從MyBatis的原始碼來理解Mybatis的第一級快取(local cache)的機制。
MyBatis有兩種快取,分別為local cache(第一級快取)及second level cache(第二及快取)。
每一個SqlSession
都有自己的local cache,彼此獨立不共享。每一次執行SQL都會將結果存入local cache中,之後若做同樣的查詢便可從local cache中取得資料而不用從資料庫取得。
而local cache的快取資料會在SqlSession
進行update,commit,rollback及close時被清除,因為這樣才能讓下一次查詢取得最新的資料。
但local cache的問題是其作用範圍僅限於SqlSession
實例本身,每一個SqlSession
有各自的local cache,所以可能會造成資料不一致的問題,因此在分散式系統中應避免使用。
例如有兩個SqlSession
分別為s1及s2。當s1第一次查詢時,MyBatis便會將查詢結果存入local cache;接著s2更新同一筆資料;s1再進行同樣的查詢便會從自己的local cache中取得快取資料,但卻是s2修改前的舊資料,而非從資料庫取得被s2修改過的資料。
SqlSession
的localCacheScope
預設為SESSION
,若要避免上述問題,應該將localCacheScope
設為STATEMENNT
,表示local cache的存續期間僅會在一個SQL敘述間,當SQL執行結束後local cache便會被清除,也就是在同一個SqlSession
中不同的SQL執行間不會共用local cache。
菜鳥工程師-肉豬
MyBatis是透過SqlSession
來存取資料庫,例如使用SqlSession.selectOne()
從資料庫查詢一個User
如下:
String resource = "idv/matt/config/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource); // mybatis配置檔位置
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 取得SqlSessionFactory
SqlSession session = sqlSessionFactory.openSession(); // 開啟(建立)一個SqlSession
try {
User user = session.selectOne("idv.matt.mapper.UserMapper.selectByName", "John"); // 查詢User
} finally {
session.close(); // 關閉SqlSession,且清除快取
}
SqlSessionFactoryBuilder().build()
建立SqlSessionFactory
的實例DefaultSqlSessionFactory
。
在呼叫DefaultSqlSessionFactory.openSession()
的過程中,會先建立Executor
的實例SimpleExecutor
(繼承BaseExecutor
)並傳入DefaultSqlSession
的建構式參數。
public class DefaultSqlSessionFactory implements SqlSessionFactory {
...
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
...
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType); // 預設建立SimpleExecutor
return new DefaultSqlSession(configuration, executor, autoCommit); // 建立Session的實例
}
...
}
}
而SqlSession.selectOne()
會轉呼叫selectList()
最後會呼叫Executor
的實例BaseExecutor.query()
來查詢,節錄原始碼如下
public class DefaultSqlSession implements SqlSession {
...
private final Executor executor;
...
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor; // SqlSessionFactory在開啟Session時便會建立新的Executor實例
this.dirty = false;
this.autoCommit = autoCommit;
}
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
...
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
...
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); // 交由Executor執行
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
...
}
BaseExecutor.query()
會先呼叫createCacheKey()
並以Mapper id,查詢偏移數(offset),筆數限制(limit),SQL語句,條件參數計算雜湊值做為local cache的key(CacheKey)。實際進入資料庫查詢前會先以key去local cache找是否有資前查詢過儲存的快取資料,若有返回快取中的資料,若無則進資料庫查詢。
若進資料庫查詢後,會以剛剛的key將查詢結果放入local cache中,所以之後在同個SqlSession
下進行同樣的查詢,則會從local cache取出資料。
public abstract class BaseExecutor implements Executor {
...
protected PerpetualCache localCache; // 這就是local cache 一級快取
protected PerpetualCache localOutputParameterCache; // 若呼叫StoredProcedure,用來儲存StoredProcedure的OUT參數
...
protected BaseExecutor(Configuration configuration, Transaction transaction) { // BaseExecutor建構式
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>(); // 執行緒安全Queue
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 建立local cache的key
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; // 進資料庫查詢前先看local cache中是否有資料
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); // 處理StoredProcedure的OUT參數快取
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // 若無資料則進資料庫查詢
}
} finally {
queryStack--;
}
...
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // localCacheScope若為STATEMENT,則查詢結束後清除local cache
// issue #482
clearLocalCache(); // 清除local cache
}
}
return list;
}
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); // 將查詢結果放入local cache
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
}
而儲存快取的localCache
類別為PerpetualCache
,此類別負責維護以HashMap<Object, Object>
儲存的快取資料。
public class PerpetualCache implements Cache {
...
private Map<Object, Object> cache = new HashMap<Object, Object>(); // local cache真正的儲存空間
public PerpetualCache(String id) {
this.id = id;
}
...
// 存放快取資料
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
// 取得快取資料
@Override
public Object getObject(Object key) {
return cache.get(key);
}
}
菜鳥工程師-肉豬
至於Spring與MyBatis整合後,SqlSession
的實例改由SqlSessionTemplate
產生SqlSession
的動態代理sqlSessionProxy
負責執行SQL。建構sqlSessionProxy
時傳入SqlSessionInterceptor
的實例中,會在每次執行SQL後close session來清除local cache,所以不會有上述的髒資料的問題。這邊的邏輯是例用代理模式(Proxy)搭配反射(Reflection)來實現如同AOP的方法切入。
public class SqlSessionTemplate implements SqlSession, DisposableBean {
...
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());
}
...
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); // 呼叫SqlSession的執行方法
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
}
}
}
}
}
整體結構大概如下
這是我第一次比較認真的去看框架的原始碼,而原本我只是想知道怎麼把local cache關掉而已。
參考:
沒有留言:
張貼留言