MyBatis 源码分析篇 8:Cache

MyBatis 的缓存有本地缓存和二级缓存两种。通过 Mybatis 文档篇 3.7:Mapper XML 之 cache 的解读我们知道,默认情况下,本地缓存是开启的且只在 Session 期间有效。二级缓存需要手动开启:在 mapper 的 XML 文件中添加一行 <cache/>,其有效期在 mapper 范围内。我们从源码的角度来做一下简单的分析。

先来看一下 MyBatis 的 cache 包的内容结构:

cache 包

其中,Cache 为接口,decorators 包和 impl 包中都是 Cache 接口的实现类。impl 包中只有一个 PerpetualCache,无论是本地缓存还是二级缓存,它都是缓存最终的栖息地;decorators 包从命名上就可以看出这个包是作装饰器用,其中的实现类比较多,且都使用了装饰器模式,用于二级缓存。另外,CacheKey 用来管理 key,TransactionalCacheManager 用来管理二级缓存。

1 CacheKey

MyBatis 是通过 Map 实现缓存的,因为 Map 的键值对形式非常适合做缓存用。所以在开始,我们需要先认识一下 CacheKey 这个类,它的作用是生成缓存的 key 的并提供对 key 做是否相等判断的方法。我们主要看它的构造方法、update() 和重写的 equals() 方法和 hashCode() 方法。

  • CacheKey()
  private static final int DEFAULT_MULTIPLYER = 37;
  private static final int DEFAULT_HASHCODE = 17;
 
  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<Object>();
  }

CacheKey 无参构造方法为这四个成员变量做初始化操作:
hashcode 用来存储缓存哈希码,初始时是 17;
multiplier 作为倍数用来计算哈希码,值为 37;
count 用来统计 key 被更新的次数,且用来参与哈希码计算,默认是 0;
ArrayList 类型的 updateList 用来存储 key 中的每一项内容。

  • update(Object object)
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

update(...) 方法的作用是填充 key 的内容项,传入的 Object 类型的参数即为要作为 key 的一部分的内容项。该方法在每次为 key 填充内容项的时候先为 count 累加一次,然后通过 checksum 和 multiplier 计算一次 hashcode,最后将该内容项添加到 updateList 中。

  • equals(Object object)
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

equals(...) 方法是用来比较两个 key 是否相同。从代码中可以看出,依次比较引用地址(地址相同则直接返回 true)、hashcode、checksum、count,最后遍历 updateList 比较每一项的内容是否相同。

  • hashCode()
  @Override
  public int hashCode() {
    return hashcode;
  }

hashCode() 只是返回了 hashcode 成员变量。hashCode() 这么简单我为什么要拿出来说?因为很重要!耐心往下看就知道了。

2 本地缓存

既然默认情况下本地缓存是在 Session 期间有效,那么我们就以下面的代码为入口进行测试,来查看 list 是如何存进缓存的, list2 是否真的没有走数据库查询而是直接从本地缓存中获取的:

    @Test
    private void testLocalCache() {
        SqlSession session = FactoryBuildByXML.getFactory().openSession(true);
        try {
            AuthorMapper mapper = session.getMapper(AuthorMapper.class);
            //第一次查询
            List<Author> list = mapper.selectByName("Sylvia");
            //第二次查询
            List<Author> list2 = mapper.selectByName("Sylvia");
        }finally {
            session.close();
        }
    }

2.1 如何存?

首先我们在 list 处打断点,步入,直到进入 CachingExecutor 的 query(...) 方法:

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

方法的第 2 行代码创建了一个 CacheKey 类型的 key。现在我们就进到 BaseExecutor 的 createCacheKey(...) 方法,看一下在执行查询语句时,MyBatis 的 key 都有哪些内容吧:

  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

我们关注调用了 CacheKey 的 update() 的地方,它依次存入了:调用的方法的完全限定名、RowBounds 的 offset 和 limit、要执行的 sql 语句、参数值和 environment 的 id。最终得到的 CacheKey 类似这样:

查询语句的 CacheKey

接着就需要把得到的 key 传入查询方法了,进入 BaseExecutor 类的 query(...) 方法:

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    //...
    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--;
    }
    //...
    return list;
  }

这个方法中,第 5 行代码即为从缓存中获取 list,因为我们执行的是第一行测试代码,那么此时该方法第一次执行,缓存一定是查不到的,所以会进入到 else 中走数据库查询。继续进入 BaseExecutor 的 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;
  }

这个方法中用到的 PerpetualCache 类型的 localCache 即为本地缓存,先将我们之前生成的 key 和一个占位符的键值对存到 localCache 中,然后执行数据库查询操作,删除 local 中关于 key 的缓存,最后将 key 和查询结果的键值对存到 localCache 中。好了,针对一次结果的查询已经存进了本地缓存中。

那么 localCache 具体是怎么实现的呢?在 BaseExecutor 类中声明了的 PerpetualCache 类型的 localCache,且在构造方法中为其初始化,初始化为 id 为 LocalCache 的 PerpetualCache 实例,具体长这样:

  protected PerpetualCache localCache;

  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    //...
    this.localCache = new PerpetualCache("LocalCache");
    //...
  }

Cache 接口是负责管理 MyBatis 缓存的,PerpetualCache 是 Cache 的实现类之一。PerpetualCache 类的实现很简单,它包含一个 Map 类型的 cache 成员变量。也就是说,针对 localCache 所做的所有操作,都是对其成员 Map 类型的 cache 的操作,比如添加。在 PerpetualCache 中,cache 长这样:

private Map<Object, Object> cache = new HashMap<Object, Object>();

对 cache 的操作长这样:

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

没错,就是这么简单!

2.2 如何取?

我们已经看了如何存,再来看看如何取。在第二行测试代码处打断点,步入,这次拿到的 key 是这样的:

执行第二行测试代码得到的 CacheKey

细心的童鞋有没有发现,这和上面第一次那个 key 的内容一毛一样!不急,我们再次进入 BaseExecutor 类的 query(...) 方法:

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    //...
    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--;
    }
    //...
    return list;
  }

第一次我们已经说过,该方法的第 5 行代码为从缓存中获取 list。它调用了 localCache.getObject(...) 方法,我们前面已经分析过,localCache 的 getObject() 其实就是对其成员变量 Map 类型的 cache 做的操作,也就是说该方法只是从 Map 中获取键对应的值。

从 Map 通过 key 获取值的依据是:调用 key 的 hashCode() 方法获取到其 hashcode 然后根据该 hashcode 生成一个 hash 做比较,然后调用 key 的 equals() 方法进一步比较 key 是否相同。

这里的 key 是 CacheKey,还记不记得我们前面关于 CacheKey 的 hashCode() 简单说明:CacheKey 重写了 hashCode() 方法返回了其成员变量 hashcode。在两次查询中生成的 CacheKey,其初始值均为 17,且在每一步的 update() 时对 hashcode 的修改完全一致。也就是说,测试代码中两次查询操作生成的 hashcode 值是相同的,equals() 比较也是相同的!

此时就不走数据库查询而是直接从缓存中获取到第一次的查询结果返回。

2.3 如何刷新?

对于缓存来讲,我们会想在做修改操作的时候它应该会刷新缓存,那么本地缓存也是这样吗?那就让我们来一探究竟。

为了测试修改时缓存是否刷新我们修改测试代码,在两次查询之间加入一次 update 操作,对比先后两次的查询结果是否一致:

    //第一次查询
    List<Author> list = mapper.selectByName("Sylvia");

    //修改
    Author author = list.get(0);
    author.setPhone("159xxxxxxxx");
    mapper.updateAuthorPhone(author);

    //第二次查询
    List<Author> list2 = mapper.selectByName("Sylvia");

其中 update 的 SQL 为:

    <update id="updateAuthorPhone">
        update author
        set
          phone = #{phone}
        where id = #{id}
    </update>

注意因为有对数据库的更新操作,不要忘了在 openSession 的时候传入参数 autoCommit 值为 true。

跳过执行第一次查询,在修改操作:mapper.updateAuthorPhone(author); 处打断点,步入,直到进入
BaseExecutor 的 update(...) 方法:

  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

我们注意到方法的倒数第二行代码对本地缓存进行了清空操作!继续跟踪代码也没有发现对缓存的继续操作。!

步入第二次查询,第二次查询获取到的缓存是空的,它确实又执行了一次数据库查询。

因为 insert(...) 和 delete(...) 方法均是调用 update() 实现的,所以也是一样的。

2.4 有效期为什么是 Session 期间?

我们知道默认情况下,本地缓存在 Session 期间有效,那么缓存的清空是什么时候呢?那么自然我们能够猜到是在关闭 session 的时候(除了上面我们看到的在更新的时候清空外)。那么我们就跟入 close() 验证一下:

session.close();

一直步入直到 BaseExecutor 的 rollback(...) 方法:

  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }

我们看到 try 块的第 1 行代码:clearLocalCache();,进入:

  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

果然,在 if 中它对 localCache 进行了 clear() 操作!

所以默认情况下,MyBatis 在关闭 Session 的时候清空本地缓存。这就解释了为什么本地缓存的有效期是 Session 期间了。

3 二级缓存

因为二级缓存需要手动开启,我们就来开启一下:

<mapper namespace="com.zhaoxueer.learn.dao.AuthorMapper">
    <cache />
    <!-- 其他略 -->
</mapper>

测试代码为:

    @Test
    private void testCache() {
        SqlSession session = FactoryBuildByXML.getFactory().openSession();
        SqlSession session2 = FactoryBuildByXML.getFactory().openSession();
        AuthorMapper mapper = session.getMapper(AuthorMapper.class);
        AuthorMapper mapper2 = session2.getMapper(AuthorMapper.class);

        List<Author> list = mapper.selectByName("Sylvia");
        session.close();
        List<Author> list2 = mapper2.selectByName("Sylvia");
        session2.close();
    }

3.1 二级缓存中 Cache 实现类的初始化

我们已经知道本地缓存是 PerpetualCache 类型的。那么二级缓存呢?MyBatis 提供的二级缓存的实现类比较多,且都使用了装饰器模式,因此嵌套层数会比较多,在正式进入测试二级缓存跟踪源码之前,先来探索一下默认情况下二级缓存实现类的初始化。

因为开启二级缓存是在 Mapper XML 中配置了 cache 元素,那么我们猜测其初始化应该同 SQL 语句等其他内容在同一时机加载:构建 SQLSessionFactory。

那么我们就来跟踪一下构建 SQLSessionFactory 的代码:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

进入 XMLMapperBuilder 的 configurationElement() 方法:

  private void configurationElement(XNode context) {
    try {
      //...
      cacheElement(context.evalNode("cache"));
      //...
    } catch (Exception e) {
     //...
    }
  }

该方法中有对 cache 元素的解析,继续跟入,进入 cacheElement(...) 方法:

  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();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

注意这个方法的最后一行代码,默认情况下,传入的 typeClass 值为
org.apache.ibatis.cache.impl.PerpetualCache,evictionClass 值为
org.apache.ibatis.cache.decorators.LruCache,flushInterval 和 size 为null,readWrite 为 true,blocking 为 false,我们并未配置 cache 的属性,所以 props 为 null。接着进入 MapperBuilderAssistant 的
useNewCache(...) 方法:

  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))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

这个方法就是创建一个合适的 Cache 并将其存入 configuration 中。那么我们继续进入 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;
  }

以及其调用的 setStandardDecorators(...) 方法:

  private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      if (readWrite) {
        cache = new SerializedCache(cache);//第二次包装
      }
      cache = new LoggingCache(cache);//第三次包装
      cache = new SynchronizedCache(cache);//第四次包装
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

这个方法前三行代码是做最终实例化前的准备工作,执行完前三行代码后,即最终实例化之前,cache 的初始状态是 PerpetualCache 类型的,那么就会进入 if 代码块,开始实例化,并对 cache 进行了四次包装:

  • 第一次包装:build(...) 方法的 for 循环中 cache = newCacheDecoratorInstance(decorator, cache);
第一次包装后的 cache
  • 第二次包装:setStandardDecorators(...) 方法的对 readWrite 判断中的 cache = new SerializedCache(cache);
第二次包装后的 cache
  • 第三次包装:setStandardDecorators(...) 方法的 cache = new LoggingCache(cache);
第三次包装后的 cache
  • 第四次包装:setStandardDecorators(...) 方法的 cache = new SynchronizedCache(cache);
第四次包装后的 cache

在默认情况下,开启二级缓存后,我们最终得到的 cache 就是这样包装了很多层且最外层为 SynchronizedCache。

需要注意的第一点是,其中包装了 SerializedCache,所以查询结果要映射到的实体类(通常为 model 类)要序列化,即实现 java Serializable 接口,否则会报错。

需要注意的第二点是,以上跟踪的代码是一次循环中对一个 Mapper XML 的解析,即一个 Mapper XML 对应会生成一个 cache。对于项目中每个 Mapper XML 均会生成其对应的二级缓存对象 cache。

3.2 如何存?

现在就可以执行二级缓存开始的测试代码了。我们先在第一次查询代码处打断点,步入代码,查看 MyBatis 是怎么存二级缓存的。

同本地缓存一样,它也是通过 CacheKey 生成一个 key,生成逻辑同本地缓存。

然后进入 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.<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);
  }

在分析本地缓存的时候,这个方法中第一行的 cache 是空的,所以直接走到了最后一行返回代码。因为从 cache 的赋值就可以看出,它是通过 Configuration 获取的,通过我们前面的分析,只有开启了二级缓存,Configuration 中才会被存入这个 cache,而它的值就是我们前面看到的通过四次包装获得的那个 cache。

因为是第一次查询,所以必然没有缓存,会进入下一个判断 list 是否为空的 if 代码块:

  if (list == null) {
      list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      tcm.putObject(cache, key, list); // issue #578 and #116
  }

第一行执行数据库查询操作并获取结果 list;第二行则是将这个 list 存储到二级缓存中。

二级缓存的存储要比本地缓存复杂一些,但其原理是一样的:使用 Map 存储。

这里的 tcm 是 TransactionalCacheManager 类型的:

private final TransactionalCacheManager tcm = new TransactionalCacheManager();

TransactionalCacheManager 是用来管理二级缓存的,它是个管理者,不做具体的工作,而是将具体操作交给 TransactionalCache 类,它也是 Cache 接口的实现类。

public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

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

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}

其中,clear()、getObject(...)、putObject(...) 方法都调用了一个方法:getTransactionalCache(...),该方法会获取或创建一个 TransactionalCache 的实例 txCache 并返回,接着这三个方法就会调用 TransactionalCache 类中相应的方法。TransactionalCache 类部分如下:

  private final Map<Object, Object> entriesToAddOnCommit;
  public TransactionalCache(Cache delegate) {
    //...
    this.entriesToAddOnCommit = new HashMap<Object, Object>();
    //...
  }

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

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

  @Override
  public Object removeObject(Object key) {
    return null;
  }

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

可以看出这里二级缓存的内容其实是 put 进了 TransactionalCache 中的 Map 类型的 entriesToAddOnCommit 成员变量中。对二级缓存的操作实质上就是对 Map 类型的 entriesToAddOnCommit 的操作。**

认识了这两个类,我们回到前面 CachingExecutor 的 query(...) 方法中:

tcm.putObject(cache, key, list);

那么此时这行代码就很好理解了,它就是把之前生成的 CacheKey 类型的 key 和数据库查询结果 list 以键值对的形式存储到了 TransactionalCache 中 Map 类型的 entriesToAddOnCommit 中。

你以为这样就完了吗?Too young too naive!曾经我也如此单纯,可是当我去跟踪获取缓存的代码的时候就开始怀疑人生了(关于获取缓存后面讲):纳尼?怎么跟存储的地方不一样?!可是就是找不到,气不气人 o(▼皿▼メ;)o。。。

事实上,MyBatis 真正对缓存的存储是在关闭 Session 时,将其序列化后的结果存储下来! 快,小本本记下来...

那我们就在关闭 Session 的代码处打个断点跟进去看看:

直到进入 CachingExecutor 的 close(...) 方法:

  @Override
  public void close(boolean forceRollback) {
    try {
      //issues #499, #524 and #573
      if (forceRollback) { 
        tcm.rollback();
      } else {
        tcm.commit();
      }
    } finally {
      delegate.close(forceRollback);
    }
  }

终于看到了熟悉的 tcm!没错, tcm 还是 CachingExecutor 类里那个 TransactionalCacheManager 类型的东东。
继续,进入 TransactionalCacheManager 的 commit(...) 方法:

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

这里呢,遍历 transactionalCaches 执行 TransactionalCache 类中的 commit(),继续进入,直到到达真相现场 TransactionalCache 类的 flushPendingEntries(...) 方法:

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

还记不记得 entriesToAddOnCommit 这个变量,就是那个我们曾经以为它就是缓存的终点的 Map 东东。这里就是对它的进一步操作。遍历 entriesToAddOnCommit 中存的缓存键值对,对每一个键值对调用 putObject(...) 存储。这里的 delegate 就是我们前面得到的那个包装了好几层且最外层为 SynchronizedCache 的 Cache。每一层都是调用的下一层的 putObject(...),比如进入 SynchronizedCache:

  public synchronized void putObject(Object key, Object object) {
    delegate.putObject(key, object);
  }

依次调用,直到进入 SerializedCache 类的 serialize() 的方法得到序列化后的缓存值:

  private byte[] serialize(Serializable value) {
    try {
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(bos);
      oos.writeObject(value);
      oos.flush();
      oos.close();
      return bos.toByteArray();
    } catch (Exception e) {
      throw new CacheException("Error serializing object.  Cause: " + e, e);
    }
  }

最后进入熟悉的 PerpetualCache 类的 putObject(...) 方法:

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

我们在本地缓存中详细分析过这个类及其方法,那么很明显,现在我们终于可以得出结论,MyBatis 将序列化后的二级缓存存到了 PerpetualCache 类的 Map 类型的 cache 成员变量中!

3.3 如何取?

上面分析完了如何存,那么如何取就很明显了,存在哪就从哪取。

进行第二次查询,在第二次查询代码处打一个断点,查看二级缓存如何获取。

进入 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.<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);
  }

这次我们重点看第二层 if 代码块中的第 2、3 行代码,从缓存中获取值:

@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);

自然它会进入 TransactionalCache 的 getObject(...) 方法:

  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    //...
  }

果然,它又从那个包装了很多层的 Cache 中取值,跟存值一样,取值也是每层调用下一层的,这里就跳过了,直接进入终点站 PerpetualCache 的 getObject(...) 方法:

 public Object getObject(Object key) {
    return cache.get(key);
  }

注意这里获得的值还不能直接返回给我们,因为它是序列化后的 byte[],需要反序列化处理,即先返回给 SerializedCache 的 getObject(...) :

  public Object getObject(Object key) {
    Object object = delegate.getObject(key);
    return object == null ? null : deserialize((byte[]) object);
  }

然后反序列化并返回还原成对象的缓存结果:

  private Serializable deserialize(byte[] value) {
    Serializable result;
    try {
      ByteArrayInputStream bis = new ByteArrayInputStream(value);
      ObjectInputStream ois = new CustomObjectInputStream(bis);
      result = (Serializable) ois.readObject();
      ois.close();
    } catch (Exception e) {
      throw new CacheException("Error deserializing object.  Cause: " + e, e);
    }
    return result;
  }

至此,我们就获得了缓存中的结果并直接返回而不会再去执行一次数据库操作。

3.4 如何刷新?

为了测试刷新,我们修改测试代码,在两次查询之间加入一次 update 操作:

    private void testCache() {
        SqlSession session = FactoryBuildByXML.getFactory().openSession(true);
        SqlSession session2 = FactoryBuildByXML.getFactory().openSession();
        AuthorMapper mapper = session.getMapper(AuthorMapper.class);
        AuthorMapper mapper2 = session2.getMapper(AuthorMapper.class);

        //第一次查询
        List<Author> list = mapper.selectByName("Sylvia");

        //修改
        Author author = list.get(0);
        author.setPhone("15364831234");
        mapper.updateAuthorPhone(author);
        session.close();

        //第二次查询
        List<Author> list2 = mapper2.selectByName("Sylvia");
        System.out.println("total records: " + list2);
        session2.close();
    }

在更新操作处打个断点,跟入到 CachingExecutor 的 update(...):

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

我们看到一行可以的代码:flushCacheIfRequired(ms);,跟进去:

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);
    }
  }

Mapper XML 中 update、insert、delete 都默认开启了缓存刷新(flushCache=true),那么会进入到 if 代码块对 TransactionalCacheManager 类型的 tcm 做 clear() 操作,跟入,直到 TransactionalCache 的 clear() 方法:

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

我们看到它清空了 entriesToAddOnCommit 这个 Map。也就是说在 update() 之前,清空了二级缓存。

继续跟入第二次查询测试的代码,发现这次确实没有获取到缓存,而是走的数据库查询。insert、delete 调用的都是 update,所以也是一样的。

3.5 二级缓存为什么是 mapper 级的?

再来简单分析一下二级缓存的作用范围。

  1. 在构建 SqlSessionFactory 的时候会解析每个 Mapper XML,并分别生成一个对应的二级缓存实例 cache。也就是说,在 Application 期间,每个 Mapper XML 都对应一个 cache。在二级缓存概念里,我们可以理解 cache 为 Mapper XML 的 id。
  2. 在每次查询前,都会根据当前调用的方法等多个条件生成一个 CacheKey 类型的 key,作为本次缓存的键。同一个 Mapper 查询方法对应的 key 是相同的(这里说相同而不是不变,事实上就算调用同一个方法,每次查询之前也都会重新生成 key,只是针对同一方法生成的 key 的 hashcode 是相同的且和 equals 比较返回 true)。
  3. 查询的结果通过 TransactionalCacheManager 的 putObject(Cache cache, CacheKey key, Object value) 方法进行两步操作:
    (1)将 Mapper XML 对应的缓存 id,即传入的参数 cache ,并绑定一个 TransactionalCache,即绑定其 Map 类型的 entriesToAddOnCommit 变量;
    (2)将传入的参数 key、value 键值对存入到(1)中绑定的 entriesToAddOnCommit 中。
  4. 在关闭本次 Session 时,遍历 3 中最后的 entriesToAddOnCommit,将其中的每个值序列化,然后将键和序列化后的值组成的键值对分别对应存入到每个 Mapper XML 的 cache 中(该 cache 是多层包装的,所以键值对实际是存到了 PerpetualCache 中 Map 类型的变量 cache 中)。

综上,因为 Mapper XML 的 cache 是 Application 期间有效的且对每个 Mapper XML 唯一,所以二级缓存是 mapper 级别的。


至此,我们根据源码基本分析完了 MyBatis 本地缓存和二级缓存的关键点。

最后再补充一点,因为开启二级缓存后,二级缓存和本地缓存都处于工作状态,那么它们之间就有个顺序问题。通过源码中缓存获取的先后位置,我们可以得出结论:先二级缓存,后本地缓存。

附:

当前版本:mybatis-3.5.0
官网文档:MyBatis
项目实践:MyBatis Learn
手写源码:MyBatis 简易实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容