Mybatis源码系列5-二级缓存

873 阅读12分钟

2020年的Flag已经立完,不知道靠不靠谱,但是挡不住对未来美好的向往

启用二级缓存二级缓存的位置二级缓存的样子二级缓存的工作原理装饰器模式事务型预缓存二级缓存的刷新总结:

Mybatis 的二级缓存相比一级缓存就复杂的多了,如果用一句话来说明Mybatis的二级缓存:

二级缓存是一个全局性,事务性,多样性的缓存

那问题来了:

二级缓存在哪里?

二级缓存长什么样子?

全局性,事务性,多样性如何体现?

工作原理是怎么样的呢?

来一探究竟

启用二级缓存

分为三步走:

1)开启全局二级缓存配置:
2) 在需要使用二级缓存的Mapper配置文件中配置二级缓存类型

  • 为每一个Mapper分配一个Cache缓存对象(使用节点配置)
  • 多个Mapper共用一个Cache缓存对象(使用节点配置);

3)在具体CURD标签上配置 useCache=true

二级缓存的位置

上文开启二级缓存步骤中,可以看出,二级缓存的配置是在xml文件中。所以想要探究二级缓存在哪里。还是得从xml文件的解析过程入手。

在[xml文件的解析]()一文讲过,Mapper配置文件的解析是由XMLMapperBuilder 解析器解析的

//-----------XMLMapperBuilder类
private void configurationElement(XNode context) {
    try {
      ...
      //解析cache标签
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      ...
    } catch (Exception e) {
    }
}
//cache标签解析
private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type""PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      String eviction = context.getStringAttribute("eviction""LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly"false);
      boolean blocking = context.getBooleanAttribute("blocking"false);
      Properties props = context.getChildrenAsProperties();
        //构建助手帮助创建Cache
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
//-----------MapperBuilderAssistant类
public Cache useNewCache(...) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);//添加到configuration一份
    currentCache = cache;//设置到当前临时变量
    return cache;
  }
public MappedStatement addMappedStatement(....){
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType).....cache(currentCache);//设置临时缓存变量
    MappedStatement statement = statementBuilder.build();//创建MappedStatement
    configuration.addMappedStatement(statement);
    return statement;
}

可以看出:

  • 二级缓存就是每一个SQL模板MappedStatement 实例的cache属性,他部署
  • 同一个namespace下的所有MappedStatement.cache属性 指向同一个cache对象。共用一个二级缓存

二级缓存的样子

二级缓存具有多样性,我们可以根据需求配置不同类型的二级缓存。
有哪些呢?

大类 类型 缓存名称 描述
基础实现 基础类 PerpetualCache 基础缓存,本质是包装了HashMap
装饰类 算法类 FifoCache 先进先出缓存
LruCache 最近最少使用
引用类 SoftCache 软引用,内存不够发生GC时删除
WeakCache 弱引用,发生GC就回收
技术增强类 SerializedCache 将缓存对象在保存前序列化和获取后反序列化
SynchronizedCache 对缓存的所有方法都加上synchronized
业务增强类 LoggingCache 记录缓存的日志,比如什么时候进来的,什么时候被删除的
TransactionalCache 事务性缓存
ScheduledCache 定期删除缓存
BlockingCache 缓存阻塞

可以看出二级缓存的种类很多。mybatis是如何组织二级缓存的呢?

重点就在参数配置上,

参数 描述
type 缓存底层实现,默认是PerpetualCache
eviction 清除策略 LRU、FIFO
flushInterval 刷新间隔,ScheduledCache
size 缓存的大小
readWrite 缓存的读写
blocking 当缓存key不存在时,是否直接查询数据库。默认false

参数的不同直接影响了二级缓存的样子

//根据属性配置来构建cache
Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
  //build方法
 public Cache build() {
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);

    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache);
    }
    return cache;
  }
//一个标准的二级缓存应该是这样的。
private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
      //如果配置了清理时间,使用ScheduledCache装饰
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
      //如果配置了读写,使用SerializedCache装饰
        cache = new SerializedCache(cache);
      }
      //使用LoggingCache装饰
      cache = new LoggingCache(cache);
      //使用SynchronizedCache 装饰
      cache = new SynchronizedCache(cache);
      if (blocking) {
      //如果配置阻塞,使用BlockingCache装饰
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

二级缓存采用装饰器模式来设计。通过不同的配置,使用不同功能的缓存装饰器来装饰基础缓存,使基础缓存具有特殊的功能。

也就是说:
二级缓存= 多级装饰器+ 基础缓存类

二级缓存的工作原理

说到二级缓存的工作原理,可以用两个知识点来总结

  • 装饰器
  • 事务型预缓存

装饰器模式

CachingExecutor
在创建DefaultSqlSession的执行器Executor时,如果开启了二级缓存功能,会创建一个装饰器CachingExecutor,来装饰基础Executor。

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    ...
    if (cacheEnabled) {
    //二级缓存装饰器
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;

  }

CachingExecutor 执行器内部创建一个TransactionalCacheManager 事务缓存管理,并使用delegate 指向基础Executor

public class CachingExecutor implements Executor {

 //目标Executor
  private Executor delegate;
  private TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
}

当开启二级缓存的情况下执行sqlsession的select方法时,首先会执行CachingExecutor的query方法。

public <E> List<E> query(...) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return 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);
           //先查询二级缓存
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
        //二级缓存没有,交给基础执行器Executor 去执行查询操作
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //放入预缓存中。
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

流程:

  • 先获取二级缓存。
  • 判断当前SQL是否开启二级缓存。
  • 开启的情况下,先去二级缓存查询。
  • 二级缓存有,直接返回
  • 二级缓存没有,交给基础执行器,走一级缓存执行过程。并把直接结果放入事务预缓存区。

事务型预缓存

在二级没有数据的情况下,通过BaseExecutor从数据库中查询到结果后,并没有直接放入二级缓存。而是先放入的事务预缓存中。

tcm.putObject(cache, key, list);

来看看这个预缓存区,如何工作。

//事务缓存管理者
public class TransactionalCacheManager {
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }
}
//事务预缓存
public class TransactionalCache implements Cache {
  private Cache delegate;
  private boolean clearOnCommit;
  private Map<Object, Object> entriesToAddOnCommit;
  private Set<Object> entriesMissedInCache;
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }
}

事务型缓存TransactionalCache,也可以理解为预缓存区,是通过装饰器模式设计的预缓存,通过delegate属性指向二级缓存,他使得二级缓存具有事务特性。

TransactionalCache 由TransactionalCacheManager事务缓存管理者,进行统一管理。

工作原理:

List<E> list = (List<E>) tcm.getObject(cache, key);
//获取当前key在二级缓存是否对应数据
 public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  //获取事务预缓存。
 private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

查询过程

  • 通过TransactionalCacheManager 查询二级缓存中是否由当前key对应的缓存数据。
  • TransactionalCacheManager 首先会检查当前当前二级缓存是否被事务缓存TransactionalCache装饰,如果没有装饰,就创建一个TransactionalCache装饰一下二级缓存。
  • TransactionalCache#getObject(Key)方法会去二级缓存中查询。

缓存过程:

//TransactionalCacheManager
public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
//TransactionalCache
public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();//刷到二级缓存中
    reset();//清空预缓存
  }
  • 通过TransactionalCacheManager#putObject 方法把从数据查询的结果放入TransactionalCache预缓存中。
  • 当Sqlsession执行commit时,执行TransactionalCacheManager#commit方法,把当前预缓存中的数据正式提交到二级缓存中。并清空预缓存区。

小结:

二级缓存的工作原理: 一个缓存执行器 + 一个预缓存 + 二级缓存

二级缓存的刷新

insert、update、delete操作后都会引发二级缓存的刷新

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);//刷新二级缓存
    return delegate.update(ms, parameterObject);
  }
private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);//清空二级缓存
    }
  }

总结:

在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor, 以及各种Cache接口的装饰器。

  • 二级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
  • 二级缓存具有丰富的缓存策略。
  • 二级缓存可由多个装饰器,与基础缓存组合而成
  • 二级缓存工作由 一个缓存装饰执行器CachingExecutor和 一个事务型预缓存TransactionalCache 完成。

如果本文任何错误,请批评指教,不胜感激 !
如果觉得文章不错,点个赞

微信公众号:源码行动
享学源码,行动起来,来源码行动