Mybatis二级缓存源码详解(二)

解析基于Mybatis 3.4.4 Release,近几个版本应该都适用。

在上一篇博客中我们详细说明了< cache>元素的解析过程和Cache对象的实例化过程,这篇博客接着往下讲。

在实例化Cache对象后,还需要弄明白其与MappedStatement的关联过程。

public class XMLMapperBuilder extends BaseBuilder {

    private void configurationElement(XNode context) {
        try {
          String namespace = context.getStringAttribute("namespace");
          if (namespace == null || namespace.equals("")) {
              throw new BuilderException("Mapper's namespace cannot be empty");
          }
          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"));
          buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
        }
    }

}

Mybatis在解析Mapper.xml时,先解析< cache>元素,实例化Cache,且将其赋值给 “currentCache” 属性后,再解析 “select|insert|update|delete” 元素。

在解析 “select|insert|update|delete” 的过程中,就能通过 “currentCache” 引用到实例化的Cache对象:

public class XMLStatementBuilder extends BaseBuilder {

    private MapperBuilderAssistant builderAssistant;

    public void parseStatementNode() {
        ......
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        // 是否刷新缓存,当不是 SELECT 类型的SQL节点时刷新
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        // 是否可使用缓存, 当是SELECT 类型的SQL节点时可使用
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        ......
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered, 
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }

}

public class MapperBuilderAssistant extends BaseBuilder {

    private Cache currentCache;

    public MappedStatement addMappedStatement(String id,SqlSource sqlSource,......) {

        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        ......
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))   // 当SQL节点类型非SELECT时, flushCacheRequired = true 
        .useCache(valueOrDefault(useCache, isSelect))        // 当SQL节点类型为SELECT时, useCache = true 
        .cache(currentCache);                                // MappedStatement关联实例化好的Cache对象
        ......
        MappedStatement statement = statementBuilder.build();
        configuration.addMappedStatement(statement);
        return statement;

    }

}

MappedStatement和Cache的关联流程也讲完了,我们接着上篇博客的CachingExecutor继续讲。

public class CachingExecutor implements Executor {

  private Executor delegate;
  private TransactionalCacheManager tcm = new TransactionalCacheManager();

  @Override
  public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 获取MappedStatement 上的缓存对象, 如果Mapper.xml里没有< cache>元素,那么返回的就是null,就不能使用二级缓存了
    Cache cache = ms.getCache();   
    if (cache != null) {
      flushCacheIfRequired(ms);
      // 如果MappedStatement的SQL元素类型是SELECT, 那么useCache == true
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        // 通过CacheKey 作为key去缓存里搜索
        List list = (List) tcm.getObject(cache, key);   
        if (list == null) {      
          // 如果缓存value不存在,那么借助delegate执行查询,然后存入tcm,这样相同的查询下次就能复用此结果了                  
          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);
  }

  // 是否要刷新缓存
  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    // 如果当前MappedStatement的SQL元素类型是insert,update,delete,那么会触发clear操作
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);
    }
  }

}

下面我们看tcm.getObject(cache, key);和tcm.putObject(cache, key, list)的内容,也就是看TransactionalCacheManager的代码:

public class TransactionalCacheManager {

  private Map transactionalCaches = new HashMap();

  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);
    // 获取Cache对应的TransactionalCache ,如果不存在那么新建一个,然后存起来以备下次复用
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}

每个DefaultSqlSession持有一个CachingExecutor,每个CachingExecutor关联一个TransactionalCacheManager ,其内部含有Cache >> TransactionalCache 的映射集合。

一个Cache代表着一个Mapper.xml内解析成的缓存对象。当使用到Cache时,如果其不存在相应的TransactionalCache,那么新建一个,然后缓存起来以备下次复用。

在一个事务内的多次数据库操作共用一个SqlSession,也就是共用一个TransactionalCacheManager ,也就是事务内使用到的Mapper的Cache都会被缓存进映射集合中。

我们先将TransactionalCacheManager 的内部逻辑讲清楚,然后接着讲起内部的 Cache >> TransactionalCache 映射:

public class CachingExecutor implements Executor {

    private Executor delegate;
    private TransactionalCacheManager tcm = new TransactionalCacheManager();

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

    public void rollback(boolean required) throws SQLException {
        try {
            delegate.rollback(required);
        } finally {
            if (required) {
                tcm.rollback();
            }
        }
    }

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

}

SqlSession在执行Commit和Rollback时,都会调用Executor的Commit和Rollback操作。

SqlSession在执行Insert,Delete,Update时会执行Executor的update方法,其内部调用flushCacheIfRequired方法,当此方法对应的Mapper.xml内有 < cache……/>节点,也就是实例化过Cache对象,且当前操作是Insert,Delete,Update 操作时,TransactionalCacheManager 清空指定的Cache。

结合上面TransactionalCacheManager 的源码,我们接着看TransactionalCache的代码:

public class TransactionalCache implements Cache {

    private Cache delegate;
    private boolean clearOnCommit;
    private Map entriesToAddOnCommit;
    private Set entriesMissedInCache;

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

    public void putObject(Object key, Object object) {
        entriesToAddOnCommit.put(key, object);           // 缓存时不直接放入Cache,而是放进一个中间缓存中,等事务提交或者回滚时再操作
    }

    public void commit() {
        if (clearOnCommit) {
            delegate.clear();
        }
        flushPendingEntries();        // 将当前事务的查询结果合并入Cache
        reset(); 
    }

    public void rollback() {
        unlockMissedEntries();
        reset();
    }

    private void reset() {
        clearOnCommit = false;
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
    }

    private void flushPendingEntries() {
        for (Map.Entry entry : entriesToAddOnCommit.entrySet()) {
            delegate.putObject(entry.getKey(), entry.getValue());                  // 合并缓存
        }
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                delegate.putObject(entry, null);
            }
        }
    }

    public void clear() {     // 当执行某个Mapper的非查询方法时,清空其相应的entriesToAddOnCommit,也就时当前事务内的查询结果缓存
        clearOnCommit = true;
        entriesToAddOnCommit.clear();
    }

    ......

} 
  

TransactionalCache 对 Cache对象做了一层装饰,在通过CacheKey获取Value时,调用Cache的getObject( ) 方法返回缓存的值。

如果缓存没有值,那么通过装饰过的Executor去执行查询方法,然后调用putObject( ) 方法,存入entriesToAddOnCommit属性中。

entriesToAddOnCommit,从字面理解,在Commit时将会被添加的Entries,也就是在事务Commit时,这些Entries会被加入Cache对象中。

看commit( ) 方法,其调用flushPendingEntries( ),将entriesToAddOnCommit里的键值对合并入装饰过的Cache中,Cache调用putObject( )最终都会合并入PerpetualCache的cache属性中。

而在rollback( ) 方法中则不会将entriesToAddOnCommit合并入Cache。

也就是说只有在事务提交后,当前事务内的查询结果才能生效;如果事务回滚,那么当前事务内的查询结果会清空。

当执行某个Mapper.xml 的Insert,Delete,Update时,会触发其Cache相应的TransactionalCache 的clear( ) 操作,将当前事务内的查询结果中间缓存清空,也就是事务内之前的查询结果都失效了。

到这里源码上的流程基本都讲完了,做个总结吧:

  1. 一级缓存和二级缓存使用的Key都是CacheKey对象,是通用的。Mybatis重写了CacheKey的hashcode和equals方法,保证了缓存的准确性。
  2. 二级缓存的总开关默认是开启的,但是每个Mapper的开关是关闭的,需要在Mapper.xml内定义< cache …… />的形式开启二级缓存
  3. 二级缓存使用了装饰者模式来对缓存添加各种功能,比如缓存淘汰,缓存数据的二进制化,日志打印,线程安全等等
  4. 二级缓存的非查询操作也会清空缓存数据
  5. 二级缓存针对事务做了一个装饰类,当前事务内的查询结果只有在事务提交后才能合并入Cache对象中
  6. 一级缓存的数据放在BaseExecutor中,随着SqlSession回收而被丢弃;二级缓存被Configuration持有,多个MappedStatement共享,会一直存在

你可能感兴趣的:(mybatis)