使用与配置
- 使用方式见Mybatis之缓存、懒加载
- 二级缓存最大的作用域是
SqlSessionFactory
,最小作用域是单个mapper.XML文件(namespace
)- 当使用
cache
标签时,缓存的作用域就是单个XML文件(namespace
) - 而使用
cache-ref
标签时,可以引用其他XML文件(namespace
)中的缓存,实现缓存共享,但仍然不能跨越SqlSessionFactory
- 当使用
关键代码
创建
- 二级缓存对象的创建要追溯到解析配置文件的mappers标签中,在对每个mapper.xml文件解析时,会拿到mapper.xml文件中的
cache
或cache-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只是出于演示的目的,并未考虑其他性能方面的优化,如果有需要,可以自行研究。