日誌輸出如下:
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.0
DEBUG - JDBC Connection [jdbc:mysql://localhost:3306/test, UserName=root@localhost, MySQL-AB JDBC Driver] will be managed by Spring
DEBUG - ==> Preparing: select id, name, age from person where id = ?
DEBUG - ==> Parameters: 15(Integer)
DEBUG - <== Total: 1
DEBUG - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606]
DEBUG - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606] from current transaction
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.0
DEBUG - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606]
DEBUG - Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606] from current transaction
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.0
DEBUG - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e63606]
sql語句打印一次,只查詢了一次數據庫,後面兩次調用沒有查詢。session期間的確存在一個緩存將結果保存起來。
先看一下MappedStatement初始化。
MapperBuilderAssistant類負責MappedStatement初始化,其中有個setStatementCache代碼中有這麼一句:
statementBuilder.cache(cache);
這裏給MappedStatement的構建者set了cache對象,即SynchronizedCache對象。
ps:SynchronizedCache對象裏面包裝了loggingCache,loggingCache包裝LRUCache,LRUCache包裝PerpetualCache。所以本質還是PerpetualCache。
每個ms就有一個cache,當我們調用查詢方法時,mybatis會選擇Executor,默認cache都是true,因此會使用CachingExecutor。
看看CachingExecutor的query();
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
這裏取出ms的cache,就是初始化的時候放入的。注意,ms的cache就是二級緩存。
tcm是TransactionalCacheManager,內部有個transactionalCaches保存了一個事物中的cache對象。
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
這裏把cache封裝到了TransactionalCache裏面去,以cache爲TransactionalCache的key。
tcm.getObject(cache, key);
ps:先調用getTransactionalCache,取得TransactionalCache,在調用TransactionalCache的getObject方法。這裏只是做了一點包裝,本質還是cache。
tcm.getObject(cache, key);就是查詢二級緩存,如果有直接返回結果,如果沒有,則查詢數據庫,保存cache到TransactionalCache。
返回結果。
一級緩存的使用是在二級緩存找不到的情況下,調用BaseExecutor的query()方法中,代碼如下:
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
localCache就是一級緩存,對應每個sqlsession一個。session關閉則消失。
如果一級緩存查找不到,就只能queryFromDatabase了。
好了,現在來解釋一下之前的問題,爲什麼在一個事物中查詢三次同一個對象,使用cache(mapper中配置<cache>),只調用一次數據庫,後面兩次查詢不查數據庫,卻也不命中二級緩存。。
爲了搞清這個問題,我又試過另一個例子。
調用service的queryBykey()三次,service的query事物屬性爲默認,readonly爲true。也就是不能提交。
打印日誌如下:
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.0
DEBUG - JDBC Connection [jdbc:mysql://localhost:3306/test, UserName=root@localhost, MySQL-AB JDBC Driver] will be managed by Spring
DEBUG - ==> Preparing: select id, name, age from person where id = ?
DEBUG - ==> Parameters: 15(Integer)
DEBUG - <== Total: 1
DEBUG - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@ecb3f1]
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.5
DEBUG - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@d306dd]
DEBUG - Cache Hit Ratio [com.demo.dao.PersonMapper]: 0.6666666666666666
此時發現後面兩次能命中。百思不得其解。不在一個事物卻能命中。
跟蹤代碼半天反覆測試 ,總算被我找到原因。
原來所有一切都是TransactionalCache惹出來的。
仔細看CachingExecutor的query代碼,在查詢二級緩存的時候用的是tcm先去找TransactionalCache然後採取getObject。
問題就在這裏,但我們在一個事物裏查詢三次,第一次查數據庫,這不必說,第二次以後會判斷二級緩存時候有。
第一次查詢完了有這麼一句。
tcm.putObject(cache, key, list);
跟進去看:
getTransactionalCache(cache).putObject(key, value);
@Override
public void putObject(Object key, Object object) {
entriesToRemoveOnCommit.remove(key);
entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object));
}
封裝cache在一個addEntry對象中去了。
put方法不是保存數據到TransactionalCache,而是保存cache到entriesToAddOnCommit;那這個entriesToAddOnCommit幹嗎用的呢?
觀察名字就知道是提交事務的時候需要用的。一個方法執行結束,事務提交,session提交,提交是層層調用的。最終調用到CachingExecutor的commit:
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
tcm的commit:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
把所有TransactionalCache提交,
public void commit() {
if (clearOnCommit) {
delegate.clear();
} else {
for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
entry.commit();
}
}
for (AddEntry entry : entriesToAddOnCommit.values()) {
entry.commit();
}
reset();
}
AddEntry的commit方法:
public void commit() {
cache.putObject(key, value);
}
就是把緩存數據放到二級緩存。
總結就是:
一個事務方法運行時,結果查詢出來,緩存在一級緩存了,但是沒有到二級緩存,事務cache只是保存了二級緩存的引用以及需要緩存的數據key和數據。當事務提交後,事務cache重置,之前保存的本該在二級緩存的數據在此刻真正放到二級緩存。
於是我們在這個方法中反覆查詢,二級緩存啓用了卻不能命中,只能返回一級緩存的數據。要想命中必須提交事務才行,第二個測試每次打開事務,查詢,釋放事務,在獲得事務查詢。所以二級緩存能命中。