【Mybatis】Mybatis源码之二级缓存

614 阅读6分钟

使用与配置

  • 使用方式见Mybatis之缓存、懒加载
  • 二级缓存最大的作用域是SqlSessionFactory,最小作用域是单个mapper.XML文件(namespace
    • 当使用cache标签时,缓存的作用域就是单个XML文件(namespace
    • 而使用cache-ref标签时,可以引用其他XML文件(namespace)中的缓存,实现缓存共享,但仍然不能跨越SqlSessionFactory

关键代码

创建

  • 二级缓存对象的创建要追溯到解析配置文件的mappers标签中,在对每个mapper.xml文件解析时,会拿到mapper.xml文件中的cachecache-ref标签
  • 先解析cache-ref标签,如果存在,则将当前namespace的缓存对象设置为标签引用的namespace的缓存对象,实现缓存共享
  • 再解析cache标签,如果存在,则将当前namespace的缓存对象更新

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())) {  // 缓存实现是PerpetualCache,即不是用户自定义的缓存实现
        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;
}
  • 在构建缓存对象时,缓存的id会设置为currentNamespace
  • 如果缓存对象是PerpetualCache,则会为其添加多层装饰,BlockingCache->SynchronizedCache->LoggingCache->SerializedCache->ScheduledCache->LruCache->PerpetualCache
  • 如果缓存对象是自定义,则只会为其添加LoggingCache装饰
    • BlockingCache:提供访问阻塞功能
    • SynchronizedCache:同步Cache
    • LoggingCache:日志功能
    • SerializedCache:序列化功能
    • ScheduledCache:定时清理功能
    • LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value
    • PerpetualCache:最基础的实现,内部有一个HashMap
  • 由于缓存对象是在创建SqlSessionFactory对象时创建,因此最大的作用域就是SqlSessionFactory

使用

  • 当开启二级缓存时,所有BaseExecutor实现类都会被CachingExecutor装饰

    // 根据配置文件中的 settings 节点cacheEnabled配置项确定是否启用缓存
    if (cacheEnabled) { // 如果配置启用该缓存
        // 使用CachingExecutor装饰实际的执行器
        executor = new CachingExecutor(executor);
    }
    

CachingExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
    // 获取MappedStatement对应的缓存,可能的结果有:该命名空间的缓存、共享的其它命名空间的缓存、无缓存
    Cache cache = ms.getCache();
    // 如果映射文件未设置<cache>或<cache-ref>则,此处cache变量为null
    if (cache != null) { // 存在缓存
        // 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) { // 该语句使用缓存且没有结果处理器
            // 二级缓存不支持含有输出参数的CALLABLE语句,故在这里进行判断
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            // 从缓存中读取结果
            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);
}
  • MappedStatement中获取当前namespace中的缓存对象

  • 判断语句执行前是否要清除二级缓存(根据flushCache属性值)

    private void flushCacheIfRequired(MappedStatement ms) {
        // 获取MappedStatement对应的缓存
        Cache cache = ms.getCache();
        if (cache != null && ms.isFlushCacheRequired()) { // 存在缓存且该操作语句要求执行前清除缓存
            // 清除事务中的缓存
            tcm.clear(cache);
        }
    }
    
  • 判断当前查询是否使用缓存(根据useCache属性值,查询默认使用,其他默认不适用)且ResultHandler为null时才使用缓存,否则不使用二级缓存

  • 使用二级缓存时,先从缓存中获取结果,如果获取到了结果,则直接返回

  • 如果缓存中没有结果,则交给被包装的执行器执行查询后,将返回的结果进行缓存,最后返回

TransactionalCacheManager#getTransactionalCache

private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
  • CachingExecutor中持有了TransactionalCacheManager对象,在使用二级缓存对象做操作时,会使用TransactionalCache对缓存对象进行包装,包装代码如上
  • 这个TransactionalCacheManager对象中持有一个HashMap,可以管理不同namespace的缓存对象

TransactionalCache#clear

  • 设置在事务提交时清楚缓存
  • 清除还未真正写入缓存的数据(临时容器)
  • 也就是说,在执行flushCacheIfRequired方法时,并没有真正清除缓存中的数据
@Override
public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
}

TransactionalCache#getObject

  • 从被包装类中读取缓存数据
  • 若缓存未命中,则记录该key到未命中缓存容器中,用于计算缓存命中率
  • 如果设置了提交时清楚缓存,则直接返回null(不走缓存),否则正常返回
@Override
public Object getObject(Object key) {
    // issue #116
    // 从缓存中读取对应的数据
    Object object = delegate.getObject(key);
    if (object == null) { // 缓存未命中
        // 记录该缓存未命中
        entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) { // 如果设置了提交时立马清除,则直接返回null
        return null;
    } else {
        // 返回查询的结果
        return object;
    }
}

TransactionalCache#putObject

  • 查询完成,向缓存中存数据时,并未直接写入到缓存,而是存放到一个临时的容器中
@Override
public void putObject(Object key, Object object) {
    // 先放入到entriesToAddOnCommit列表中暂存
    entriesToAddOnCommit.put(key, object);
}

提交事务时发生了什么?

时序图

sequenceDiagram
participant A as DefaultSqlSession
participant B as CachingExecutor
participant C as TransactionalCacheManager
participant D as TransactionalCache

A ->> B : commit
B ->> C : commit
C ->> D : commit

TransactionalCache#commit

public void commit() {
    if (clearOnCommit) { // 如果设置了事务提交后清理缓存
        // 清理缓存
        delegate.clear();
    }
    // 将未写入缓存的操作写入缓存
    flushPendingEntries();
    // 清理环境
    reset();
}
  • 如果设置了提交事务后清除缓存,则清除缓存

  • 将临时容器中缓存的数据写入缓存中(未命中缓存容器中的数据会以空值的形式一起写入缓存)

    private void flushPendingEntries() {
        // 将entriesToAddOnCommit中的数据写入缓存
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        // 将entriesMissedInCache中的数据写入缓存
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                delegate.putObject(entry, null);
            }
        }
    }
    
  • 重置clearOnCommit标识,清空临时容器

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

跨SqlSessionFactory使用二级缓存

思路很简单,只需要定义一个缓存容器,其生命周期不依赖于SqlSessionFactory的创建即可。

  • 自定义MyCache类,此类需要实现org.apache.ibatis.cache.Cache接口

    public class MyCache implements Cache {
    
        private final String id;
    
        static Map<Object, Object> cache = null;
    
        static {
            cache = new HashMap<>();
        }
    
        public MyCache(String id) {
            this.id = id;
        }
    
        @Override
        public String getId() {
            return id;
        }
    
        @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) {
            cache.remove(key);
            return null;
        }
    
        @Override
        public void clear() {
            cache.clear();
        }
    
        @Override
        public int getSize() {
            return Integer.valueOf(cache.size()+"");
        }
    
        @Override
        public ReadWriteLock getReadWriteLock() {
            return null;
        }
    }
    
  • 由于每次创建在SqlSessionFactory时,都会对MyCache进行实例化,并且都是调用MyCache的有参构造函数进行实例化的

    // CacheBuilder类
    
    private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
        Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
        try {
            // 通过有参构造函数创建缓存类实例
            return cacheConstructor.newInstance(id);
        } catch (Exception e) {
            throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
        }
    }
    
    private Constructor<? extends Cache> getBaseCacheConstructor(Class<? extends Cache> cacheClass) {
            try {
                // 获取缓存实现类的有参构造函数
                return cacheClass.getConstructor(String.class);
            } catch (Exception e) {
                throw new CacheException("Invalid base cache implementation (" + cacheClass + ").  "
                        + "Base cache implementations must have a constructor that takes a String id as a parameter.  Cause: " + e, e);
            }
        }
    
  • 因此不能将MyCache类设置为单例,只能将内部的Map设置为单例,这里就使用了静态代码块的方式对Map进行了初始化,这样每次创建SqlSessionFactory时,虽然创建的MyCache对象不同,但是其内部的Map都是共享的同一个,这样便实现了二级缓存跨SqlSessionFactory使用。

    static Map<Object, Object> cache = null;
    
    static {
        cache = new HashMap<>();
    }
    
  • 最后,将MyCache配置到cache标签中即可使用。

    <!-- 开启二级缓存-->
    <cache type="org.apache.ibatis.z_run.util.MyCache"
           size="1024"
           eviction="LRU"
           flushInterval="120000"
           readOnly="true"/>
    

这里使用Map只是出于演示的目的,并未考虑其他性能方面的优化,如果有需要,可以自行研究。