Mybatis(六): 缓存模块详解
文章目录
今天我们进行
Mybatis
相关内容的第六篇文章,在本篇文章中我们将介绍下
Mybatis
的缓存模块。
相信大家都听过Mybatis
的缓存,应该都知道其有一级
和二级
缓存,那么Mybatis
的缓存的结构是怎样的以及其是如何工作的呢?相信看完本篇内容后你能找到答案。
1 模块结构
缓存模块位于org.apache.ibatis.cache
包下,这个包结构如下:
Mybatis
的缓存模块使用了装饰器模式,这里不再对这个设计模式进行介绍了,不了解的同学可以自行去学习,Mybatis
的缓存模块结构如下:
Cache
缓存的顶层接口 装饰器模式中的组件impl
这个包下定义了Cache
的基本实现类 目前只有一个PerpetualCache
实现 装饰器模式中的具体组件decorators
里面定义了多种类型的装饰器 其实也是Cache
的实现类,并维护了一个Cache
对象BlockingCache
阻塞装饰器,只能由一个线程进行查询操作FifoCache
限制大小按照先进先出的规则删除缓存的装饰器LoggingCache
日志装饰器 具有记录操作次数和打印日志的功能LruCache
限制大小按照LRU
规则删除缓存的装饰器ScheduledCache
按照一定频率删除缓存的装饰器SerializedCache
序列化装饰器SoftCache
软引用装饰器SynchronizedCache
同步装饰器TransactionalCache
二级缓存缓冲区WeakCache
弱引用装饰器
CacheKey
Mybatis
中缓存的key对应的对象TransactionalCacheManager
二级缓存缓冲区管理器 里面会调用TransactionalCache
对应的方法
2 Cache
和PerpetualCache
Cache
接口是缓存的顶层接口,这个接口中定义了缓存应该具有的功能,其源码如下:
public interface Cache {
// 获取缓存对象的id
String getId();
// 向缓存中添加数据 key是CacheKey对象 value是查询结果
void putObject(Object key, Object value);
// 查询缓存
Object getObject(Object key);
// 删除缓存
Object removeObject(Object key);
// 清空缓存
void clear();
// 获取缓存的数量
int getSize();
default ReadWriteLock getReadWriteLock() {
return null;
}
}
PerpetualCache
是Cache
接口的基本实现,其使用的是一个Map
实现了缓存的功能,其源码如下:
public class PerpetualCache implements Cache {
// 缓存的标识
private final String id;
// 存储缓存的map
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@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) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
3 装饰器
装饰器其实也是Cache
的一个实现类,这些类都在decorators
路径下,在上面我们已经列举了相应的类,这里我们分别介绍下这些装饰器的实现逻辑。
3.1 BlockingCache
BlockingCache
是一个阻塞装饰器,这个装饰器可以保证在同一时刻只有一个线程能调用查询缓存的方法。
这个类的属性如下:
// 阻塞时长
private long timeout;
// Cache对象
private final Cache delegate;
// 存储key对应的CountDownLatch对象 用于实现加锁逻辑
private final ConcurrentHashMap<Object, CountDownLatch> locks;
BlockingCache.putObject
的逻辑如下:
public void putObject(Object key, Object value) {
try {
// 添加缓存
delegate.putObject(key, value);
} finally {
// 释放锁
releaseLock(key);
}
}
这里的逻辑是调用Cache
对象的putObject
方法,之后调用releaseLock
方法,这个方法的逻辑如下:
private void releaseLock(Object key) {
// 删除map中的key
CountDownLatch latch = locks.remove(key);
if (latch == null) {
throw new IllegalStateException("Detected an attempt at releasing unacquired lock. This should never happen.");
}
// 释放同步状态
latch.countDown();
}
BlockingCache.getObject
方法的逻辑如下:
@Override
public Object getObject(Object key) {
// 获取key的锁
acquireLock(key);
// 查询缓存
Object value = delegate.getObject(key);
if (value != null) {
// 释放锁 删除掉locks中的key
releaseLock(key);
}
return value;
}
// 获取锁
private void acquireLock(Object key) {
// 创建CountDownLatch对象
CountDownLatch newLatch = new CountDownLatch(1);
while (true) {
// 如果不存在则添加返回null 如果存在返回当前值
CountDownLatch latch = locks.putIfAbsent(key, newLatch);
// 没有其他线程在读数据
if (latch == null) {
break;
}
try {
if (timeout > 0) {
// CountDownLatch中的方法 在一定时间内获取到同步状态
boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
// 未获取到抛出异常
throw new CacheException(
"Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} else {
// 获取同步状态
latch.await();
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
}
}
看到这里相信大家应该能明白这个类是如何实现阻塞功能的了吧,通过向ConcurrentHashMap
中添加数据,添加成功则表示获取锁成功,否则调用CountDownLatch.await()
方法等待其他线程释放同步状态。
3.2 FifoCache
这个装饰器是限制缓存的大小(默认为1024),当缓存数量超过设定值后,会按照先进先出的规则来删除最早添加的缓存。这个类的属性如下:
//被装饰的Cache对象
private final Cache delegate;
// 记录key进入缓存的顺序
private final Deque<Object> keyList;
// 缓存项的最大数量
private int size;
我们只看下这个类的putObject()
方法,其源码如下:
@Override
public void putObject(Object key, Object value) {
// 判断是否超过大小 并清理之前的缓存
cycleKeyList(key);
delegate.putObject(key, value);
}
private void cycleKeyList(Object key) {
// 添加到尾部
keyList.addLast(key);
if (keyList.size() > size) {
// 从头部移除
Object oldestKey = keyList.removeFirst();
// 从缓存中删除
delegate.removeObject(oldestKey);
}
}
在这个类中维护了一个队列,用来记录缓存添加的顺序,当添加数据时会先在这个队列的尾部添加数据,如果这是长度超过了设定值,则获取队列的第一个缓存的key,然后从缓存中移除。
3.3 LruCache
这个装饰器也会限制缓存的大小,其会按照近期最少使用
的方法进行缓存的删除,其属性如下:
// 被修饰的缓存对象
private final Cache delegate;
// 使用的是LinkedHashMap 调用setSize时会为其设值
private Map<Object, Object> keyMap;
// 用来记录最少使用的key
private Object eldestKey;
这个类中有一个setSize()
的方法,会在这个类的构造方法中进行调用,源码如下:
public void setSize(final int size) {
// 创建一个LinkedHashMap
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
// LinkedHashMap.put()方法中会调用这个方法判断是否超长
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
这个类的putObject
方法逻辑如下:
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
private void cycleKeyList(Object key) {
// keyMap中添加数据
keyMap.put(key, key);
// 如果最少使用的key不为空
if (eldestKey != null) {
// 删除最少使用的缓存
delegate.removeObject(eldestKey);
// 设置为空
eldestKey = null;
}
}
这个类的getObject
方法逻辑如下:
public Object getObject(Object key) {
keyMap.get(key); // touch
return delegate.getObject(key);
}
这个类的LRU
算法是通过LinkedHashMap
实现的,重写了LinkedHashMapremoveEldestEntry
方法,当添加元素时会调用到这个方法,在这里判断是否超过长度,如果超过则删除最少使用的缓存。
3.4 LoggingCache
这个装饰器会记录查询缓存和查询到缓存的数量,这个类的字段如下:
// 日志对象
private final Log log;
// Cache对象
private final Cache delegate;
// 获取缓存的数量
protected int requests = 0;
// 获取到缓存的数量
protected int hits = 0;
这个类的getObject
方法如下:
public Object getObject(Object key) {
// requests加一
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
// 查询到缓存 hits加一
hits++;
}
if (log.isDebugEnabled()) {
// 如果支持debug 打印日志
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}
3.5 ScheduledCache
这个装饰器会以一定的频率清空缓存,这个类的字段如下:
// 缓存装饰器
private final Cache delegate;
// 清理间隔 默认为一小时
protected long clearInterval;
// 上次清理时间
protected long lastClear;
这个实现的也比较简单哈,当调用putObject
、getObject
和removeObject
方法时都会调用clearWhenStale
方法,判断下是否需要清除缓存,这个方法如下:
private boolean clearWhenStale() {
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}
3.6 SerializedCache
这个装饰器提供了序列化的功能,在存储缓存时会将value进行序列化,当查询缓存时再进行反序列化,这两个方法逻辑如下:
// 序列化
private byte[] serialize(Serializable value) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(value);
oos.flush();
return bos.toByteArray();
} catch (Exception e) {
throw new CacheException("Error serializing object. Cause: " + e, e);
}
}
// 反序列化
private Serializable deserialize(byte[] value) {
SerialFilterChecker.check();
Serializable result;
try (ByteArrayInputStream bis = new ByteArrayInputStream(value);
ObjectInputStream ois = new CustomObjectInputStream(bis)) {
result = (Serializable) ois.readObject();
} catch (Exception e) {
throw new CacheException("Error deserializing object. Cause: " + e, e);
}
return result;
}
3.7 SoftCache
和WeakCache
这两个装饰器的实现逻辑很相似,唯一不同的时一个是一个是将value封装为SoftReference
,一个封装为WeakReference
。我们这里就看下SoftCache
的实现逻辑。
SoftCache
的成员变量如下:
// 会将最近使用的缓存添加到该队列,避免GC回收
private final Deque<Object> hardLinksToAvoidGarbageCollection;
// 引用队列 用于记录被GC回收的缓存项
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;
// 强连接的个数 默认为256 当长度超过该值后会从hardLinksToAvoidGarbageCollection移除数据
private int numberOfHardLinks;
这个缓存中存储数据会将value封装成一个SoftEntry
对象,这个类如下:
private static class SoftEntry extends SoftReference<Object> {
private final Object key;
SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);
this.key = key;
}
}
SoftCache.putObject
方法逻辑如下:
public void putObject(Object key, Object value) {
// 清除已经被GC的数据
removeGarbageCollectedItems();
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
putObject
方法会先调用清除被GC
的缓存,然后再进行数据保存,清除GC
的缓存逻辑如下:
private void removeGarbageCollectedItems() {
SoftEntry sv;
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}
SoftCache.getObject
的逻辑如下
public Object getObject(Object key) {
Object result = null;
@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
if (softReference != null) {
result = softReference.get();
if (result == null) {
// 如果已经被GC回收,从缓存中移除
delegate.removeObject(key);
} else {
// See #586 (and #335) modifications need more than a read lock
synchronized (hardLinksToAvoidGarbageCollection) {
// 添加到头部
hardLinksToAvoidGarbageCollection.addFirst(result);
// 如果大小超过限制
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
// 移除最早添加的数据
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
3.8 SynchronizedCache
这个装饰器是在方法上增加synchronized
关键字实现的同步功能,这里不展开说明了。
4 CacheKey
CacheKey
是作为缓存key的对象,在Mybatis
中的缓存并不是使用了一个简单的字符串类型,而是将查询封装成一个CacheKey
的对象作为key进行缓存的。这个类的字段如下:
private final int multiplier;
// hashCode
private int hashcode;
// 校验和
private long checksum;
// updateList集合的大小
private int count;
// 由该集合中的所有对象共同决定两个CacheKey是否相同
private List<Object> updateList;
这个类的代码比较简单,update
的逻辑如下:
public void update(Object object) {
// 计算object的hashCode
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// count值增加1
count++;
// checkSum值加上baseHashCode
checksum += baseHashCode;
// baseHashCode*count
baseHashCode *= count;
// 计算hashCode
hashcode = multiplier * hashcode + baseHashCode;
// 添加到updateList中
updateList.add(object);
}
重写的equals
方法如下:
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
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;
}
5 Mybatis
中缓存的使用
Mybatis
中的缓存分为一级缓存和二级缓存两个,一级缓存默认便是生效的,二级缓存默认没有生效。二级缓存的范围要比一级缓存的范围大,一级缓存的使用范围是SqlSession
级别的,二级缓存的范围是namespace
级别的。
若想使用Mybatis
的二级缓存需要满足如下两点:
Mybatis
配置文件中配置cacheEnable
属性为true
,默认便是true
- 在映射文件中配置
<cache>
标签
5.1 Mybatis
时对缓存的处理
在这部分我们看下,Mybatis
在启动时对缓存做了那些内容,在这里我们直接介绍使用的地方,具体的流程我们这里先不进行介绍。
5.1.1 cacheEnable
的使用
cacheEnable
属性的使用位置在Configuration.newExecutor
方法中,当开启这个配置后,会创建CachingExecutor
执行器,方法的源码如下:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 是否启用二级缓存
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
CachingExecutor
是Mybatis
中会使用二级缓存的执行器,在这个类中维护了一个TransactionalCacheManager
对象。
5.1.2 cache
节点的解析
cache
节点的解析入口在XMLMapperBuilder.cacheElement()
方法,这个方法的逻辑如下:
private void cacheElement(XNode context) {
// <cache>节点不为空
if (context != null) {
// 获取type属性 默认为PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
// 通过别名获取对应的类型
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 获取eviction属性 默认为LRU
String eviction = context.getStringAttribute("eviction", "LRU");
// 获取eviction别名对应的类型
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 获取flushInterval属性
Long flushInterval = context.getLongAttribute("flushInterval");
// 获取size属性
Integer size = context.getIntAttribute("size");
// 获取redaOnly属性 默认为false
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 获取blocking属性 默认为false
boolean blocking = context.getBooleanAttribute("blocking", false);
// 获取子节点数据
Properties props = context.getChildrenAsProperties();
// 创建Cache对象并添加到Configuration.caches中
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 创建Cache对象
Cache cache = new CacheBuilder(currentNamespace)// namespace作为缓存id
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 将cache添加到configuration中
configuration.addCache(cache);
// 记录当前命名空间使用的cache
currentCache = cache;
return cache;
}
CacheBuilder
是缓存的创建者,这个类的属性如下:
// cache的id
private final String id;
// Cache接口的实现类
private Class<? extends Cache> implementation;
// 装饰器列表
private final List<Class<? extends Cache>> decorators;
// cache大小
private Integer size;
// 清理时间周期
private Long clearInterval;
// 是否可读写
private boolean readWrite;
// 配置信息
private Properties properties;
// 是否阻塞
private boolean blocking;
CacheBuilder.builder()
方法逻辑如下:
public Cache build() {
// 如果未指定实现和装饰器 这个方法进行设置默认值
setDefaultImplementations();
// 创建Cache对象
Cache cache = newBaseCacheInstance(implementation, id);
// 设置cache的属性
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
// 判断cache的类型 如果是PerpetualCache类型 添加装饰器
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 添加标准装饰器
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 如果不是LoggingCache的子类 添加LoggingCache
cache = new LoggingCache(cache);
}
return cache;
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
// 如果size不为空且有对应的setter方法,设置size
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 如果clearInterval不为空 添加定时清理装饰器
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// 如果支持读写 添加Serialized装饰器
if (readWrite) {
cache = new SerializedCache(cache);
}
// 添加LoggingCache
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);
}
}
5.2 缓存的使用
缓存的使用是在执行器的查询方法中,执行器具体的逻辑我们在执行器部分再进行介绍,我们这里只看下使用缓存的逻辑,入口为CachingExecutor.query()
方法,方法的逻辑如下:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 获取Cache对象
Cache cache = ms.getCache();
// cache对象不为空
if (cache != null) {
// 判断是否需要刷新缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 查询二级缓存
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);
}
BaseExecutor.query()
方法的逻辑如下:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
6 总结
今天的文章到这里就结束了,在本文中我们介绍了如下内容:
- 缓存模块的结构
- 缓存的实现及各种装饰器的使用
- 如何启用二级缓存
Mybatis
启动时对缓存的处理Mybatis
执行SQL
时如何使用缓存
大家可以想想如果我不想用Mybatis
自带的缓存实现,我应该如何去自定义一个缓存实现类并使用呢?
欢迎关注公众号:Bug搬运小能手