Mybatis源码解析(七 完结)--缓存设计

184 阅读8分钟

这个章节中的源码在第二章和第三章部分出现过,最好先看一遍。

二级缓存

总所周知的,在进行数据库查询的时候,首先要先查询缓存。查询缓存的操作首先发生在执行器CachingExecutor。

Executor默认外层的CachingExecutor嵌套这内层的SimpleExecutor。

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    //SIMPLE 就是普通的执行器;
    //REUSE 执行器会重用预处理语句(PreparedStatement); 
    //BATCH 执行器不仅重用语句还会执行批量更新。 
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    //cacheEnabled表示全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。可以在settings中配置
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

那么要开启二级缓存,cacheEnabled要为true。这样才能生成CachingExecutor执行缓存操作。在进行查询操作时,首先会去获取缓存,注意MappedStatement.getCache()这个方法。MappedStatement是用来存储我们的sql语句信息,也是在解析mapper.xml是生成的。

  //CachingExecutor.query
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      //这个首先获取二级缓存,默认是不开起的,需要在手动在mapper.xml配置
    Cache cache = ms.getCache();
    //没有配置就是null
    if (cache != null) {
      //判断缓存是否需要刷新,flushCache是select的一个参数,如果为true,那么每次都会清空缓存
      flushCacheIfRequired(ms);
      //判断是否使用缓存,如select就能配置useCache属性,而insert,update,delete就没有
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //尝试取缓存中查找,如果没有,调用查找方法
        //tcm并不是直接的缓存,可以把它认为是事务缓存的管理器,用来记录当前cache命中的缓存和没有命中的缓存。
        //通过它间接获取cache的值
        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);
  }

如果这个ms.getCache()返回空,if里面的语句就都不执行了,就不会使用二级缓存,那这个cache什么时候为空呢?既然MappedStatement是在解析配置文件的阶段就生成的,那么就要回到解析时候的源码。

二级缓存的创建

  //XMLMapperBuilder.configurationElement
  private void configurationElement(XNode context) {
    try {
      //mapper的缓存信息,命名空间等会被临时保存到MapperBuilderAssistant中,最后把这些公用的信息在存到MappedStatement中
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      //MapperBuilderAssistant该类用来协助处理mapper.xml,并保存一些中间信息
      //解析每个mapper.xml都会创建一个自己的builderAssistant
      builderAssistant.setCurrentNamespace(namespace);
      //引用其它命名空间的缓存配置。
      cacheRefElement(context.evalNode("cache-ref"));
      //该命名空间的缓存配置。 
      cacheElement(context.evalNode("cache"));
      //该节点已经被废弃,尽量不要使用
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //在解析增删改查节点时,每个节点都会生成一个mapperStatement对象并保存到configuration类中.
      //mapperStatement保存这这个节点的全部信息,如id,fetchSize,timeout
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

在创建cacheElement命名空间的的缓存时,就换创建一个Cache,这个cache属于这个mapper.xml,并保存在builderAssistant中

  private void cacheElement(XNode context) {
    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();
      //创建缓存
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
  public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {
    //创建缓存
    //根据cache中的eviction会创建不同类型的缓存
    //LRU(默认,LruCache) – 最近最少使用:移除最长时间不被使用的对象。
    //FIFO(FifoCache) – 先进先出:按对象进入缓存的顺序来移除它们。
    //SOFT(SoftCache) – 软引用:基于垃圾回收器状态和软引用规则移除对象。
    //WEAK(WeakCache) – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
    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);
    //给builderAssistant中的参数currentCache赋值
    currentCache = cache;
    return cache;
  }

生成mappedStatement代码,会直接获取属于该mapper的cache而不是去创建新的。

//XMLStatementBuilder.parseStatementNode
public void parseStatementNode() {
   //解析一堆select上的标签,代码略,详细在第二章查看
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
  public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {

    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        //记录标信息,查询之前是否刷新缓存
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        //记录标信息,是否使用缓存
        .useCache(valueOrDefault(useCache, isSelect))
        //获取builderAssistant中缓存
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }
    public Builder cache(Cache cache) {
      mappedStatement.cache = cache;
      return this;
    }

回到之前的查询代码,小节一下,要使用二级缓存需满足以下几个条件:

  1. settings中的配置cacheEnabled要为true(默认true),这样才能生成CacheExecutor,执行二级缓存相关代码。
  2. 在mapper.xml中需要配置cache标签,才能创建属于这个mapper的Cache对象,每个mapper都有一个自己的缓存。根据配置的参数会选择不同的缓存。
  3. select中的useCache标签要为true(默认值:对 select 元素为 true),这样才能在通过后续的逻辑判断。

二级缓存的结构

通过上文可知,我们可以选4种类型的缓存。那么如果防止缓存数据过多导致内存溢出呢?由于默认选择的缓存机制是LruCache(最近最少使用),那么就用这个举例。

缓存的接口,每个缓存都要实现这个接口,封装了基础的获取清除元素等方法。

public interface Cache {

  String getId();

  void putObject(Object key, Object value);

  Object getObject(Object key);

  Object removeObject(Object key);

  void clear();

  int getSize();

  default ReadWriteLock getReadWriteLock() {
    return null;
  }

LruCache也是要实现这个接口,但是可以发现,它除了自己本身维护了一个keyMap用来缓存数据,还有一个类型为delegate的Cache。也许更应该将LruCache看成一个缓存的装饰器。

public class LruCache implements Cache {
  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;
  //...方法略
 }

那么内部的这个Cache是什么类型的缓存呢?从刚才的创建缓存的代码可以找到答案。

  public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
		//...
        .build();
        //...
  }

内层的Cache是PerpetualCache,这个缓存可以看作是缓存的最基本实现,一级缓存就是使用它作为缓存的容器。而LruCache是在其上的进一步包装。

public class PerpetualCache implements Cache {
  private final String id;
  private Map<Object, Object> cache = new HashMap<>();
  //...方法略
}

而在LruCache之上,根据配置文件还可能包装多层的缓存。

  //CacheBuilder.build
  public Cache build() {
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    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 {
      //设置重新缓存的大小(初始化1024)
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      //flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      //readOnly(只读)默认属性false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。
      if (readWrite) {
        cache = new SerializedCache(cache);
      }
      //固定嵌套的两层,日志缓存和同步缓存
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      //blocking,数据块将逐块计算,使得存储器访问是一个具有高内存局部性的小邻域。
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

就这样一层一层的嵌套,最后的缓存就变成了多层的缓存结构。

二级缓存数据的清理

还是用LruCache举例。

public LruCache(Cache delegate) {
  this.delegate = delegate;
  setSize(1024);
}

在其构造方法中可以看出,这里规定了最大容量为1024

public void setSize(final int size) {
  keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
    private static final long serialVersionUID = 4267176411845948333L;

    @Override
    protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
    //如果当前的map大小大于1024
      boolean tooBig = size() > size;
      if (tooBig) {
        //记录最老的键值对
        eldestKey = eldest.getKey();
      }
      return tooBig;
    }
  };
}

它选择了Map类型的容器作为缓存,并重写了removeEldestEntry方法。该方法通过给定一定的条件,如果返回true,那么就移除最老的键值对。

  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }
  private void cycleKeyList(Object key) {
    //keyMap超过容量会自行移除
    keyMap.put(key, key);
    if (eldestKey != null) {
      //delegate是PerpetualCache,需要手动移除
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

每当往缓存中添加新数据时,都会判断最老的键值对是否存在(如果容量达到1024,再往里添加就存在了),那么就将老的键值对移除,这样就能保证缓存数量最大为1024.

一级缓存

BaseExecutor是所有Executor的父类,当执行query方法时会用到一级缓存。也叫做localCache本地缓存。这个缓存在BaseExector创建的时候一起创建,并在一次会话结束后随之一起销毁。

//BaseExecutor构造方法
//一级缓存使用的是PerpetualCache,该类的结构在上文
  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

在进行查询之前,首先会在localCache中进行查询,这个没有用到配置文件的信息,所以一级缓存是不能关闭的。

  //BaseExecutor.query
  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.");
    }
    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();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }