Mybatis (三) 一级缓存和二级缓存的源码分析

文章目录

  • 1、mybatis 缓存体系
  • 2、一级缓存
  • 2、二级缓存
  • 总结

1、mybatis 缓存体系

Mybatis (三) 一级缓存和二级缓存的源码分析_第1张图片

  • 首先当一二级缓存同时开启的时候,首先命中二级缓存;
  • 一级缓存位于 BaseExecutor 中不能关闭,但是可以指定范围 STATEMENT、SESSION;
  • 整个二级缓存虽然经过了很多事务相关的组件,但是最终是落地在 MapperStatement 的 Cache 中(Cache 的具体实例类型可以在 mapper xml 的 cache type 标签中指定,默认 PerpetualCache),而 MapperStatement 和 namespace 一一对应,所以二级缓存的作用域是 mapper namespace;
  • 在使用二级缓存的时候,如果 cache 没有命中则向后查找,然后查询的结果不是直接放到 cache 中,而是首先放到TransactionCache 的本地缓存中,这里区分
    entriesToAddOnCommit、entriesMissedInCache 是为了统计命令率,最后在 sqlSession
    commit 的时候,才会将 TransactionCache 的本地缓存提交到 cache 中,此时 cache 才是对其他
    sqlSession 可见的;
  • 此外当需要分布式缓存的时候,就需要将二级缓存放到 JVM 之外,这里可以实现 cache 接口编写自己的 cache,此时在实现的
    cache 中就可以使用 ehcache、redis 等外部缓存进行操作;

2、一级缓存

mybatis 的一级缓存一般情况很少使用,其原因主要有两个:

  • 一级缓存的生命周期同 SqlSession,所以容易出现脏读;
  • 一级缓存的 cache 的实现只能是 PerpetualCache,所以不能指定容量等设置;

所以通常都设置一级缓存的范围为:STATEMENT,就是每次查询后会清除缓存

<setting name="localCacheScope" value="SESSION"/>

启用可以配置:

<setting name="cacheEnabled" value="true"/>

一般默认情况下是启用的

源码分析:

直接进入BaseExecutor的query方法中,具体请看注释

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) { throw new ExecutorException("Executor was closed."); }
  
  // 查询的时候一般不清楚缓存,但是可以通过 xml配置或者注解强制清除,queryStack == 0 是为了防止递归调用
  if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); }
  List<E> list;
  try {
    queryStack++;
    // 首先查看一级缓存
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // 没有查到的时候直接到数据库查找
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    // 延迟加载队列
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  	  // 一级缓存本身不能关闭,但是可以设置作用范围 STATEMENT,每次都清除缓存
      clearLocalCache();
    }
  }
  return list;
}

queryFromDatabase方法源码:


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

2、二级缓存

mybatis 二级缓存要稍微复杂一点,中间多了一步事务缓存:

  • 首先无论是查询还是更新,都会按要求清空缓存 flushCacheIfRequired,默认更新清空,查询不清空,也可以在 xml或者注解中指定;
  • 查询的时候,先查缓存,命中返回,未命中查一级缓存、数据库,然后回填事务缓存,注意这里不是直接填充到缓存中;此时的事务缓存对任何的SqlSession 都是不可见的,因为自己查询的时候也是直接查询的目标缓存;
  • 更新就直接委托给目标 Executor 执行;
  • 最后 SqlSession 执行commit 的时候,将事务缓存刷新到目标缓存中;

开启是要在mapper文件中设置:

<cache eviction="FIFO" flushInterval="60000" size="2" readOnly="true"/>

缓存配置:

此外还可以配置各种二级缓存策略,比如大小,刷新间隔时间,淘汰策略等,这里主要就是使用了 Cache 接口的装饰者模式:

在这里插入图片描述

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

源码分析:

直接看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, boundSql);
        @SuppressWarnings("unchecked")
        // 从二级缓存中获取
        List<E> list = (List<E>) tcm.getObject(cache, key);
        // 不命中缓存
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 查询到数据,后放入缓存中,这个时候是放入到entriesToAddOnCommit中的
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

查看tcm.getObject的方法发现是从delegate中获取缓存的

 public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

查看一下TransactionalCache由那些字段

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
 
  // 实际的事务缓存数据
  private final Cache delegate;
  private boolean clearOnCommit;
  // 是待添加的数据,需要等待事务缓存提交之后才会添加到delegate中去
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;
}

我们知道是从哪里获取二级缓存的了,那么二级缓存是在什么时候放进去的呢?
查看sqlSession.commit方法可以知道最后会调用CachingExecutor的commit方法,源码如下:

  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

最后便会跑到TransactionalCache的flushPendingEntries方法,在这里会将事务缓存放进去,然后其他sqlSession才能获取的到缓存

private void flushPendingEntries() {
	// 把所有带添加的二级缓存添加到缓存中去
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

到此二级缓存也分析的差不多了。

总结

mybatis 一级缓存的生命周期和 SqlSession 是一样的,通常情况下不建议使用一级缓存,通常将一级缓存范围设置为 STATEMENT;
使用 mybatis 二级的时候,务必记得 SqlSession.commit ,否则二级缓存是不生效的;
在配置 mybatis 分布式二级缓存的时候,要确保缓存淘汰等策略是可以用于分布式缓存的;

你可能感兴趣的:(Mybatis,mybatis)