Mybatis源码分析七之Cache缓存

一、一级缓存

我们知道mybatis的一级缓存是默认开启的,不需要配置,也没有相应的配置功能去关闭。所以有必要了解一级缓存的使用,以及实现原理,这样我们才能更好的使用其缓存机制。

前文分析知道,mybatis的缓存实现逻辑在Executor中实现的,所以这里我们还是要重新分析一下BaseExecutor中的query方法。

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    //如果查询栈为0并且设置了flushCache=true,那么先清理缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
    //就是调用map集合的clear方法
      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();//执行延迟操作
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

从上可知,查询的时候会先从localCache(PerpetualCache实例)中取是否存在,如果缓存存在直接取值,不存在查询数据库,然后存缓存。还有一个设置就是在configuration配置文件中设置localCacheScope=STATEMENT即可,默认是SESSION,此两种方式可以清楚一级缓存。

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

所以针对同一个SqlSession和相同的查询语句,会直接使用一级缓存来实现。这里查询和存缓存的时候有两个操作queryStack的自增和自减,以及占位符
queryStack: 查询栈,查询开始的时候会判断当前值是否为0以及flushCache是否为true来清楚缓存,然后进行加一操作,查询完成之后,在finally语句块中进行减一操作,只有当满足queryStack=0的时候才会执行延迟加载和再次判断是否需要清空缓存。这样的设置是为了在单线程环境下,解决嵌套查询下不清除缓存操作,以及不执行延迟加载操作。
占位符: localCache.putObject(key, EXECUTION_PLACEHOLDER)在进行数据库查询之前,先用一个占位符,保存在缓存中,等查询完成之后再更新具体的值。这里设计是为了解决循环嵌套的问题,也就是子查询配置了和主查询一样的查询,那么在进行子查询的时候,就会从缓存中取出这个占位符,那么在转换成List集合时候就会报错,从而退出循环嵌套查询。
以上两个都是在单线程环境下有效,所以在多线程环境下要注意线程安全问题。

二、二级缓存

二级缓存需要配置才能生效,也就是设置< cache/>标签即可,也可以通过useCache=false来关闭当前查询语句的二级缓存功能。在分析Executor的时候我们知道。Executor默认会用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, boundSql);
        @SuppressWarnings("unchecked")
        //从缓存中取
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //设置缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

仔细分析可以发现一级缓存和二级缓存整体逻辑相似,但是不同的地方就是缓存值保存的载体不一样,一级缓存是通过Cache实例来保存,二级缓存是通过CachingExecutor实例中的TransactionalCacheManager来保存,这里就存在一个问题,二级缓存是怎么在不同的SqlSession实例中共享缓存的呢?我们先看它的putObject方法。

  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

先获取到一个TransactionalCache对象(从名称来看,这是带事务的缓存器),然后再调用其putObject方法,TransactionalCache也是Cache接口的一个实现,它被保存在TransactionalCacheManager的一个transactionalCaches的map集合中。键是Cache对象,也就是当前执行语句中配置的二级缓存对象。
TransactionalCache的putObject方法

  public void putObject(Object key, Object object) {
  //这是一个Map<Object, Object>集合
    entriesToAddOnCommit.put(key, object);
  }

到这里,似乎还是没有实现二级缓存功能,但是仔细查看集合名称,似乎需要提交之后才生效。所以我们需要查看其CachingExecutor的commit方法->tcm.commit()

  public void commit() {
  //执行当前所有事务缓存提交
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

txCache.commit()-> flushPendingEntries()

  private void flushPendingEntries() {
  //也就是把entriesToAddOnCommit集合中的所有值都用委托缓存对象保存
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
  }

delegate这个实例是通过MappedStatement获取,这是在解析映射文件的时候,解析器cache标签的时候,得到的,可以发现最外层是SynchronizedCache->LoggingCache->SerializedCache->LruCache->PerpetualCache,这是一种级联调用。所以这里的putObject方法会按照这样的顺序调用下去,然后在SerializedCache中会对value进行序列化,所以要使用二级缓存,缓存对象必须要实现Serializable否则就会报错。最后都是把结果保存到PerpetualCache对象的HashMap集合中。getObject的时候也会存在一个反序列化过程。

二级缓存之所以能够在多个不同的SqlSession实例共享,是因为缓存对象是通过MappedStatement对象获取的,这是一个映射文件级别的缓存对象,只要是这个映射文件中的任何查询语句拿到的这个对象都是一样的,又因为映射文件是共享的,所以这样达到了二级缓存的效果。

因为二级缓存是事务级别的缓存,所以只有当事务提交之后才会生效,也就是只有事务提交之后才会真正的保存缓存值。

这里有个缓存值序列化的问题,是通过SerializedCache来实现的,这是在CacheBuilder的setStandardDecorators方法中通过判断是否可读写readWrite来实现的,默认是可读写,也可以通过修改设置成readOnly=true,这样就不会使用序列化,我们在分析映射文件解析的时候,也讲过设置成readOnly,那么返回的都是同一个缓存对象,如果某个SqlSession实例修改了这个对象,就会对其它实例造成影响,所以这是不安全的,但是性能很好。相反,如果可读写,就采用了序列化,每次返回的是对象的一个拷贝,所以相对安全,但是性能降低,这个可以根据具体的需求去设置。

三、Cache接口

以上简单的分析了一级缓存和二级缓存的实现逻辑,分析结果来看最终的缓存结果都是保存在Cache实例中

public interface Cache {
    //缓存Id
    String getId();
   //保存缓存值
    void putObject(Object var1, Object var2);
	//获取缓存值
    Object getObject(Object var1);
	//移除缓存值
    Object removeObject(Object var1);
// 清空缓存
    void clear();
//获取缓存大小
    int getSize();
//获取读写锁
    default ReadWriteLock getReadWriteLock() {
        return null;
    }
}

接口的方法简单也好理解,难于理解的在于它的实现类。

  1. BlockingCache:带阻塞式缓存,这就是类似于生产者消费者模型
  2. FifoCache:先进先出缓存,这是缓存的失效策略,也就是在保存缓存的时候,会判断当前缓存值是否超过了设置值(默认是1024,如果超过了就移除,移除规则就是先进先出)
  3. LoggingCache:带日志的缓存,取缓存的时候,会打印出缓存命中率
  4. LruCache:最少使用原则,通过LinkedHashMap来实现,移除最少使用的缓存(默认大小1024)
  5. PerpetualCache:永久缓存,也就是缓存不会被清理
  6. ScheduledCache:带定时器的缓存,默认是清理间隔是一个小时,可以设置flushInterval清理间隔
  7. SerializedCache:带序列化的缓存
  8. SoftCache:软引用缓存,这是通过jdk自带的软引用方式,也就是当内存不足的时候会清楚缓存
  9. WeakCache:弱引用缓存,这是通过jdk自带的弱引用方式,也就是当进行垃圾回收的时候会清楚缓存
  10. SynchronizedCache:同步缓存,也就是加了锁的缓存
  11. TransactionalCache :事务级缓存,也是实现二级缓存的关键

以上就是大致分析了Cache接口的实现类,具体实现逻辑不是很难,难点在于我们怎么合理的使用这些缓存。

四、总结

Mybatis默认会开启全局缓存功能,一级缓存是默认开启的,二级缓存需要配置对应的cache标签,如果我们想关闭一级缓存就需要设置flushCahe=true或者设置localCacheScope=STATEMENT,这样就会让一级缓存失效。二级缓存如果想关闭的话需要配置useCache=false,一级缓存保存对象是PerpetualCache,二级缓存需要不停的封装,二级缓存还可以配置对应的失效策略等。

以上,有任何不对的地方,请指正,敬请谅解。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜鸟+1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值