水煮MyBatis(十三)- 二级缓存
前言
相比一级缓存而言,二级缓存的实现类就很丰富了,还支持开发者自定义缓存实现。上一章提到过缓存执行器:CachingExecutor,就是专门为二级缓存准备的。默认的二级缓存驱除策略:LruCache。
什么是LRU? LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的数据置换算法,选择最近最久未使用的数据予以淘汰。该算法赋予每个数据一个访问字段,用来记录一个数据自上次被访问以来所经历的时间 t,当须淘汰一个数据时,选择现有数据中其 t 值最大的,即最近最少使用的数据予以淘汰。
为什么避免使用二级缓存
就我所知,二级缓存其实是有很多问题的,比如
- 多表联合查询,A JOIN B ,b表删除了记录,但是在A的命名空间里,B的数据任然存在;
- 命名空间隔离,A表的数据,在B表执行时被级联删除,但是在A的命名空间中,A对应的数据任然存在;
二级缓存清除时机
二级缓存的有效范围是命名空间,其实也是应用级别的,其缓存删除一般有下面几个情况:
- 达到设定的过期时间,【ScheduledCache】;
- 达到缓存容量上限,@CacheNamespace的size参数,默认1024;
- 执行insert、update和delete语句时,清空缓存中所有数据。注意,是清空;
也就是说,如果没有触发上述三个条件,在应用运行期间,缓存会一直存在。
二级缓存分类
除了驱除功能的四个实现类【FIFO,LUR,SOFT,WEAK】,其余的实现都可以嵌套使用
功能介绍
- BlockingCache:如果缓存中没有指定的key,则等待,直到数据返回,可选;
- SynchronizedCache:增删改查操作设置为同步方法,必选类;
- LoggingCache:输出debug级别的命中概率日志,命中率计算方式:hits / requests,必选类;
- SerializedCache - 对缓存对象进行数列化,可选;
- ScheduledCache - 发生读写、getSize、删除操作时,验证数据是否过期,如果过期则移除,可选;
缓存多层嵌套图
构建代码
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);
}
}
缓存执行器
在前面的章节中,有反复提到执行器【Executor】,这是mybatis里的一个接口,目前是非线程安全的。面对缓存场景,Mybatis提供了特定的执行器【CacheExecutor】,其余所有的执行器都继承BaseExecutor。
执行器初始化代码:
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);
}
// 如果配置中支持缓存,则返回CachingExecutor对象
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 加载拦截器插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
逻辑很简单,一连串的if/else,关键是cacheEnabled配置,如果设置为true,则返回CachingExecutor对象。而CachingExecutor只是一个装饰器,内部还是BaseExecutor的继承类去实现执行逻辑。
mybatis.configuration.cache-enabled = true
如何开启
这里只基于注解进行分析,不介绍XML配置,大概原理是一样的。
初上手的开发者,可能认为有了【mybatis.configuration.cache-enabled】配置,就算开启二级缓存了。其实不然,还需要配置命名空间缓存,否则不会到二级缓存中查询。如下:
@Repository
@CacheNamespace(blocking = true, flushInterval = 10, size = 10)
public interface ImageInfoMapper extends Mapper<ImageInfo> {
// ...
}
有了这个注解,在ImageInfoMapper初始化时,会进行缓存配置解析。类【】
private void parseCache() {
CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
// 如果配置了命名空间
if (cacheDomain != null) {
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
Properties props = convertToProperties(cacheDomain.properties());
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
@CacheNamespace属性
- type:cache使用的类型,默认是PerpetualCache,这在上一章中说过。
- eviction: 备选LRU 最近最少使用,移除时间最长的对象,FIFO先进先出,SOFT软引用(移除基于垃圾回收器状态和软引用规则的对象),WEAK弱引用
- flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
- size: 最多缓存对象的个数。
- readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
- blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
@CacheNamespaceRef
这个配置之前是@CacheNamespace里面的一个属性,后续版本升级以后,被单独拎出来作为一个独立注解使用。代表共用其他的缓存命名空间,多个命名空间的操作使用的是同一个Cache。
如何使用
在CachingExecutor执行器中,查询数据方法里,在调用BaseExecutor的查询之前,会提前在二级缓存中查找是否存在缓存数据,如果没有,则进行后续查询操作。
public <E> List<E> query(...){
Cache cache = ms.getCache();
// 刷新缓存
flushCacheIfRequired(ms);
// 读取Mapper方法上,@Options注解里的useCache配置
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 关键就是这一行,tcm.getObject(cache, key),从指定的cache中,查找指定的key
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 调用baseExecutor里的查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
// 调用baseExecutor里的查询
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
大概说一下逻辑
- 获取配置在ms初始化时候的cache配置,就是上文提到的那个嵌套cache对象;
- 和一级缓存一样,如果在Mapper方法上配置了刷新策略【@Options(flushCache = Options.FlushCachePolicy.TRUE)】,则会清除缓存,当然,也就用不到二级缓存了;
- 读取Mapper方法上,@Options注解里的useCache配置,如果不使用缓存,也会直接跳到baseExecutor里的查询;
- 读取二级缓存,是通过CacheExecutor里的tcm对象,从指定的cache中,查找指定的key。也是在这里才真正用到二级缓存。
缓存选择
与执行器类似,除了PerpetualCache,其余缓存实现,使用的都是装饰器模式,内部嵌套了一个缓存实现对象。
@CacheNamespace(blocking = true, flushInterval = 1800000, size = 10, eviction = FifoCache.class)
- blocking = true,则嵌套BlockingCache;
- readWrite,嵌套SerializedCache;
- clearInterval,嵌套ScheduledCache;
- eviction,选择后面则四个驱除策略: LruCache 、FifoCache 、SoftCache 、WeakCache;
默认二级缓存
在mybatis里,下面这两个地方都设置了默认的缓存实现
@CacheNamespace
在注解中,也定义了默认的缓存实现。可以看出,对于mybatis来说,PerpetualCache是真正存储数据的地方,其余实现类只是为了管理缓存数据,定义多种多样的数据过期策略,前提是达到缓存的容量上限,默认size是1024。
- implementation:缓存数据真正保存的地方;
- eviction:定义缓存驱除策略。
public @interface CacheNamespace {
/**
* 一般就是PerpetualCache,可以自己实现
*/
Class<? extends Cache> implementation() default PerpetualCache.class;
/**
* 选择后面则四个驱除策略: LruCache 、FifoCache 、SoftCache 、WeakCache;
*/
Class<? extends Cache> eviction() default LruCache.class;
...
}
CacheBuilder
在CacheBuilder类中,定义了默认的缓存设定。
- 一级缓存默认实现:PerpetualCache
- 二级缓存默认实现:LruCache
private void setDefaultImplementations() {
if (implementation == null) {
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
decorators.add(LruCache.class);
}
}
}
序列化的小问题
如果在@CacheNamespace注解里配置了readWrite = true
,则表实体类必须实现序列化接口【Serializable】,因为readWrite开启以后,会对实体进行序列化以后再保存到内存。如果不实现序列号接口,会出现如下异常:
nested exception is org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: XXXX
转载自:https://juejin.cn/post/7244713671230668858