之前在这篇文章中:mybatis中的缓存
写过在MyBatis中通过@CacheNamespace注解开启二级缓存,本文将讲下具体的实现
本文参考:MyBatis二级缓存
Cache接口
在一级缓存中,sqlSession使用HashMap来进行缓存的存储,二级缓存默认也是用HashMap进行缓存的存储,为了保证数据的持久性也需要定时将缓存的数据刷新至硬盘,也可以使用第三方的集成工具如:Redis,来进行缓存的持久化,但是内存的空间是有限的,如果不停地向HashMap中put K-V键值对,那么肯定有一时刻会导致内存不足产生OOM。所以为了解决这个问题,我们必须限定可存储缓存的容量,如果容量满了那么就必须执行清空操作:通过溢出淘汰算法 例如:FIFO先进先出算法、LRU最近最少使用算法等。同时也需要给缓存的K-V键值对设置保质期,定时进行清理工作(MyBatis默认为不过期,即永久有效),同时二级缓存允许跨线程使用,所以二级缓存还必须保证线程安全问题
此外,还包括 命中率的统计和序列化需求等
MyBatis中只用了一个顶层的接口来实现上述功能,就是Cache接口
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;
public interface Cache {
String getId();
void putObject(Object var1, Object var2);
Object getObject(Object var1);
Object removeObject(Object var1);
void clear();
int getSize();
default ReadWriteLock getReadWriteLock() {
return null;
}
}
Cache采用了装饰器模式+责任链模式来实现功能,在下文中细说
@CacheNamespace注解
@CacheNamespace(implementation = MybatisRedisCache.class,
eviction = FifoCache.class,
size = 2048,
readWrite = false,
blocking = true,
properties = {@Property(name = "timeout", value = "3600L")})
@CacheNamespace注解的属性如下:
public @interface CacheNamespace {
Class<? extends Cache> implementation() default PerpetualCache.class;
Class<? extends Cache> eviction() default LruCache.class;
long flushInterval() default 0L;
int size() default 1024;
boolean readWrite() default true;
boolean blocking() default false;
Property[] properties() default {};
}
其中的implementation和evicition属性对应图中的PerpetualCache和LRUCache的位置
implementation属性
implementation属性中的类需要实现Cache接口,主要的功能就是实现具体的存储功能,下文中将默认的二级缓存存储在HashMap改成了redis中
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
@Slf4j
public class MybatisRedisCache implements Cache {
private String id;
private Long timeout = 3600L;
private TimeUnit timeUnit = TimeUnit.SECONDS;
private RedissonClient redissonClient;
public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(final Object key, final Object value) {
RMap<String, Object> map = getRedissonClient().getMap(id);
if (value != null) {
map.fastPut(key.toString(), value);
map.expire(timeout, timeUnit);
} else {
log.warn("-------------{}------{}-----value空---", id, key);
}
}
@Override
public Object getObject(final Object key) {
Map<String, Object> map = getRedissonClient().getMap(id);
try {
return map.get(key.toString());
} catch (Exception e) {
log.error("-------------{}------{}--------", id, key, e);
return null;
}
}
@Override
public Object removeObject(Object key) {
RMap<String, Object> map = getRedissonClient().getMap(id);
return map.fastRemove(key.toString());
}
@Override
public void clear() {
RMap<String, Object> map = getRedissonClient().getMap(id);
map.delete();
}
@Override
public int getSize() {
RMap<String, Object> map = getRedissonClient().getMap(id);
return map.size();
}
@Override
public ReadWriteLock getReadWriteLock() {
return new ReentrantReadWriteLock();
}
private RedissonClient getRedissonClient() {
if (redissonClient == null) {
//手动获取redis连接类
redissonClient = ApplicationContextHolder.getBean(RedissonClient.class);
}
return redissonClient;
}
public void setTimeout(String timeout) {
this.timeout = Long.valueOf(timeout);
}
}
properties属性
properties属性就是在implementation属性中的类初始化的时候设置具体的值,本文中就是把MybatisRedisCache类中的timeout属性设置为3600毫秒
eviction属性
eviction属性就是具体的垃圾回收策略,mybatis内置的有四种,分别是:
LruCache(最近最少回收,移除最长时间不被使用的对象)
FifoCache(先进先出,按照缓存进入的顺序来移除它们)
SoftCache(软引用,移除基于垃圾回收器状态和软引用规则的对象)
WeakCache(弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象)
flushInterval属性
缓存的有效时间,单位是秒,0为永久有效
size属性
缓存容量,超过该值开始执行垃圾回收策略
readWrite属性
readWrite属性为是否开启序列化,默认开启,序列化前后值相同,但序列化ID不同
序列化的缺点就是耗时,如果想取消序列化可以通过设置该属性为false取消序列化,取消序列化后线程是不安全的,原因是MyBatis默认所有从缓存中获取数据的操作都是只读操作,不会修改数据,所以会把数据在缓存中的引用直接返回,可能读取脏数据
blocking属性
当blocking属性为false的时候
如果多个线程执行同一条sql(sql语句和参数都相同)时,会去多次查询数据库,这对于并发量大的查询接口是不好的,会给数据库造成很大的压力,浪费数据库服务器资源。
当blocking属性为true的时候
还是多个线程执行同一条sql的情况,当发现缓存中没有数据的时候,会由获得锁的那个线程去查询数据库,然后把数据存入缓存,当锁释放后,其他的线程就会直接查询缓存返回