日志输出如下:
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重置,之前保存的本该在二级缓存的数据在此刻真正放到二级缓存。
于是我们在这个方法中反复查询,二级缓存启用了却不能命中,只能返回一级缓存的数据。要想命中必须提交事务才行,第二个测试每次打开事务,查询,释放事务,在获得事务查询。所以二级缓存能命中。