mybatis 高速缓存和二级缓存

最近研究标志映射,提到了最理想的保存标识映射的地方时工作单元,其次是会话。联想到mybatis的sqlsession。于是做了一个测试,在一个事务方法中不断调用mapper的查询方法,观察日志打印,看究竟查询了几次数据库。

日志输出如下:

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);


getTransactionalCache(cache)返回TransactionalCache对象,然后调用它的put,是什么呢


@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重置,之前保存的本该在二级缓存的数据在此刻真正放到二级缓存。

于是我们在这个方法中反复查询,二级缓存启用了却不能命中,只能返回一级缓存的数据。要想命中必须提交事务才行,第二个测试每次打开事务,查询,释放事务,在获得事务查询。所以二级缓存能命中。


最后说一下mybatis自带缓存的目的不是提高多少性能,而是为了在一个事务中返回结果同一个对象。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章