乐趣区

关于源码分析:mybatis框架下一二级缓存

上篇文章提到查问时会用到缓存,其内置的两级缓存如下:

// 一级缓存,在 executor 中,与 sqlsession 绑定
// org.apache.ibatis.executor.BaseExecutor#localCache
// 指向 org.apache.ibatis.cache.impl.PerpetualCache#cache
private Map<Object, Object> cache = new HashMap<>();

// 二级缓存,在 MappedStatement 中(对应 mapper.xml 中的一个 crud 办法),周期与 SqlSessionFactory 统一
org.apache.ibatis.mapping.MappedStatement#cache
// 最终也指向了 org.apache.ibatis.cache.impl.PerpetualCache#cache
private Map<Object, Object> cache = new HashMap<>();
  • 一、二级缓存都是查问缓存,select 写入,insert、update、delete 则革除
  • 一、二级缓存均指向org.apache.ibatis.cache.impl.PerpetualCache#cache,实质是一个 HashMap
  • 一、二级缓存 Key 的计算形式统一,均指向org.apache.ibatis.executor.BaseExecutor#createCacheKey,Key 的实质:statement 的 id + offset + limit + sql + param 参数
  • 一级缓存生命周期和 SqlSession 统一,默认开启;二级缓存申明周期和 SqlSessionFactory 统一,需手动开启
  • 雷同 namespace 应用同一个二级缓存;二级缓存和事务关联,事务提交数据才会写入缓存,事务回滚则不会写入

接下来通过源码别离来看一下。

一级缓存

一级缓存的生命周期是 sqlSession;在同一 sqlSession 中,用雷同 sql 和查问条件屡次查问 DB 状况,非首次查问会命中一级缓存。

一级缓存默认是开启的,如果想敞开须要减少配置

// == 如果不设置,默认是 SESSION(后续的源码剖析会波及这里)<setting name="localCacheScope" value="STATEMENT"/>

以查询方法作为入口

org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);
    // == 计算 CacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // == 查问中应用缓存
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

CacheKey 计算

org.apache.ibatis.executor.BaseExecutor#createCacheKey
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {CacheKey cacheKey = new CacheKey();
    // == 调用 update 办法批改 cache
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    // value 是参数
    cacheKey.update(value);
    return cacheKey;
}

从这里就能够猜测到,CacheKey 和 statement 的 id、offset、limit、sql、param 参数无关

进入 CacheKey 验证这个猜想:

### CacheKey 类 ###

// 默认 37
private final int multiplier;
// 默认 17
private int hashcode;
private long checksum;
private int count;

private List<Object> updateList;

public void update(Object object) {int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    
    // -- 批改几个属性值
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    // -- updateList 新增对象
    updateList.add(object);
}

public boolean equals(Object object) {
    // -- 比拟几个属性值
    if (hashcode != cacheKey.hashcode) {return false;}
    if (checksum != cacheKey.checksum) {return false;}
    if (count != cacheKey.count) {return false;}
    // -- 挨个比拟 updateList 中的对象
    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;
}

查问中应用缓存

org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    try {
      queryStack++;
      // == 1. 先从 localCache 获取数据
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } 
      // == 2. 缓存中无数据,从数据库查问
      else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    }

    // ## 如果 scope 设置成 STATEMENT 类型,会清理一级缓存
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // 清理缓存
      clearLocalCache();}
    return list;
}

持续察看 代码 2 地位:

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 {
    // == 查问 DB 逻辑
    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;
}

综上,查问过程会向 localCache 中寄存查问后果。
只不过 设置 scope 为 STATEMENT 时,每次都会清空缓存——这就是一级缓存生效的机密

增删改清理缓存

insert 和 delete 办法都会执行 update:

public int insert(String statement) {return insert(statement, null);
}

public int delete(String statement) {return update(statement, null);
}

于是察看 update 即可:

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

二级缓存

二级缓存须要关上开关:

  • 第 1 步
<setting name="cacheEnabled" value="STATEMENT"/>
  • 第二步

同时在 mapper.xml 中减少标签

<cache/> 

默认的,二级缓存的 key 是 namespace,如果要援用其它命名空间的 Cache 配置,能够应用如下标签:

<cache-ref namespace="xxx"/>

CachingExecutor

二级缓存的入口在 executor 创立地位:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    Executor executor;
    if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);
    } else {
      // 默认创立 SimpleExecutor
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      // == 开启二级缓存状况,应用装璜器模式用 CachingExecutor 包了一层
      executor = new CachingExecutor(executor);
    }
    return executor;
}

察看结构器里做了什么

// 属性相互赋值
public CachingExecutor(Executor delegate) {
  this.delegate = delegate;
  delegate.setExecutorWrapper(this);
}

赋值后 CachingExecutor 和 SimpleExecutor 的关系如下

晓得这一点后,咱们来查看 CachingExecutor 的 query 办法:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);
    // == 调用 delegate 的 createCacheKey 办法(后面曾经剖析过)CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    // == 二级缓存的查问
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

察看 query 办法的实现

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // ## A. 通过 MappedStatement 获取 cache
    Cache cache = ms.getCache();
    if (cache != null) {
      // 缓存刷新
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);
        // -- 1. 通过 tcm 获取查问后果
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // -- 2.tcm 中无后果,通过原 executor 查问(一级缓存 +jdbc 逻辑)list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // -- 3. 查问后果最终放入 tcm 中
          tcm.putObject(cache, key, list); 
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

// ## B.tcm 指向这里
TransactionalCacheManager tcm = new TransactionalCacheManager();

整顿逻辑很简略,但又有两个问题困扰到我

  1. 通过 MappedStatement 获取到的二级缓存 cache(代码 A 地位),什么时候初始化的?
  2. 二级缓存和 tcm(TransactionalCacheManager)之间有什么分割?

二级缓存初始化

沿着 cache 倒推,能追溯到 Mapper 解析。

残缺调用链如下(当作温习了):

// 创立 SqlSessionFactory
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties)
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
// configuration 解析
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
// 解析 mapper
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement{
    // == 二级缓存的配置援用(执行 namespace)cacheRefElement(context.evalNode("cache-ref"));
    // == 二级缓存的开启
    cacheElement(context.evalNode("cache"));
}

org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement
org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache{
    // == 二级缓存创立
    Cache cache = new CacheBuilder(currentNamespace)
        // -- Cache 实现是 PerpetualCache
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        // -- 包装器用了 LruCache
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();}

看下二级缓存的整个装璜链(盗图)

SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。

二级缓存和 TransactionalCacheManager 的关系

TransactionalCacheManager 类:

// ## 保护一个 map,key 是 Cache,value 是 TransactionalCache
Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

public Object getObject(Cache cache, CacheKey key) {
    
           // ## 1. 此办法会在 transactionalCaches 中建设 k - v 关系
    return getTransactionalCache(cache)
                ⬇⬇⬇⬇⬇⬇
                transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
            
            // == 2. 从二级缓存中获取
            .getObject(key);
}

再察看 TransactionalCache

// == 二级缓存
private final Cache delegate;
// == 二级缓存清理标记
private boolean clearOnCommit;

// ####   以下两个汇合能够了解为用来寄存长期数据   ####
// == 等事务提交时,须要退出二级缓存的对象
private final Map<Object, Object> entriesToAddOnCommit;
// == 二级缓存中不存在的对象 key
private final Set<Object> entriesMissedInCache;

public void putObject(Object key, Object object) {
    // 对象记录到 entriesToAddOnCommit 中
    entriesToAddOnCommit.put(key, object);
}

public Object getObject(Object key) {
    // 从二级缓存获取
    Object object = delegate.getObject(key);
    if (object == null) {
      // 二级缓存中不存在,在 entriesMissedInCache 记录 key
      entriesMissedInCache.add(key);
    }
}

这里可能看出,二级缓存和 Transaction(事务)有很深的瓜葛。
那么具体有什么瓜葛?

  • 事务提交

察看 TransactionManager 的 commit 办法:

org.apache.ibatis.cache.TransactionalCacheManager#commit
org.apache.ibatis.cache.decorators.TransactionalCache#commit
public void commit() {
    // == 刷新对象
    flushPendingEntries();}

private void flushPendingEntries() {for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      // == 对象从 entriesToAddOnCommit 刷新到二级缓存中
      delegate.putObject(entry.getKey(), entry.getValue());
    }
}

此处能证实,事务提交时对象从一个长期汇合 entriesToAddOnCommit 刷新至二级缓存

  • 事务回滚

再察看回滚办法

org.apache.ibatis.cache.decorators.TransactionalCache#rollback
public void rollback() {unlockMissedEntries();
    // == 重置,将长期汇合数据清理
    reset();}

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

附录

P6-P7 常识合辑

退出移动版