1.二级缓存概述
前一章节介绍了一级缓存即会话级别的缓存,同一个会话SqlSession才能共享缓存。如果多个 SqlSession 需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询。
二级缓存也称作是应用级缓存,与一级缓存不同的,是它的作用范围是整个应用,而且可以跨线程使用。所以二级缓存有更高的命中率,适合缓存一些修改较少的数据。在流程上是先访问二级缓存,再访问一级缓存。
当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个
共同的 cache
,也就是二级缓存被多个 SqlSession 共享,是一个
全局的变量
2.开启二级缓存
二级缓存默认是不开启的,需要手动开启二级缓存开启二级缓存的条件也是比较简单
-
通过直接在 MyBatis 配置文件中通过
<settings> <setting name = "cacheEnabled" value = "true" /> </settings>
-
在 Mapper 的xml 配置文件中加入
<cache>
标签 -
useCache 为true
-
flushCache 为false
设置 cache 标签的属性
-
type
: 指定自定义缓存的全类名(实现Cache 接口即可),PerpetualCache
默认值
-
eviction
: 缓存回收策略,有这几种回收策略。默认值为**LRU **- LRU – 最近最少回收,移除最长时间不被使用的对象
- FIFO – 先进先出,按照缓存进入的顺序来移除它们
- SOFT – 软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK – 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
-
flushinterval
缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值 -
size
: 缓存存放多少个元素 -
readOnly
: 是否只读;
true 只读
,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。
读写(默认)
:MyBatis 觉得数据可能会被修改。默认值为
false
-
blocking
: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。默认值为
false
3.二级缓存初始化
解析mapper xml文件二级缓存的关键点。mybatis首先进行解析的是
cache-ref
标签,其次进行解析的是
cache
标签。二级缓存解析后,存入Cache
// 配置cache-ref
cacheRefElement(context.evalNode("cache-ref"));
// 配置cache
cacheElement(context.evalNode("cache"));
3.1解析cache
// 对cache标签的属性解析
private void cacheElement(XNode context) {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 构建Cache对象放入Configuration对象中
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
// 使用了构建器模式,一步一步构建Cache 标签的所有属性,最终把 cache 返回。
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
// 定义缓存的实现类
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
// 缓存回收策略
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
// 缓存刷新间隔
.clearInterval(flushInterval)
// 缓存存放多少个元素
.size(size)
// 是否只读
.readWrite(readWrite)
// 若缓存中找不到对应的key,是否会一直blocking
.blocking(blocking)
.properties(props)
.build();
// 把cache放入configuration对象中
// Configuration类中的字段 key为namespace
// protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
configuration.addCache(cache);
// 构建MapperStatement时使用
currentCache = cache;
return cache;
}
public Cache build() {
setDefaultImplementations();
// 根据指定的type,implementation初始化Cache类型
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 如果是默认的PerpetualCache,则使用装饰器模式 装饰基本的缓存Cache
// 如LRU、FIFO等都是Cache的装饰器模式的类
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())) {
cache = new LoggingCache(cache);
}
return cache;
}
// 责任链
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);
}
}
3.2解析cache-ref
private void cacheRefElement(XNode context) {
if (context != null) {
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
public class MapperBuilderAssistant extends BaseBuilder {
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
public MappedStatement addMappedStatement() {
//...
.useCache(valueOrDefault(useCache, isSelect))
// 给MappedStatement设置缓存对象
.cache(currentCache);
//...
}
}
4.二级缓存的功能
-
存储【核心功能】
-
内存:最简单就是在内存当中,不仅实现简单,而且速度快。内存弊端就是不能持久化,且容易有限。
-
硬盘:可以持久化,容量大。但访问速度不如内存,一般会结合内存一起使用。
-
第三方集成:在分布式情况,如果想和其它节点共享缓存,只能第三方软件进行集成。比如Redis.
-
-
溢出淘汰【核心功能】
无论哪种存储都必须有一个容易,当容量满的时候就要进行清除,清除的算法即溢出淘汰机制。
-
FIFO:先进先出
-
LRU:最近最少使用
-
WeakReference: 弱引用,将缓存对象进行弱引用包装,当Java进行gc的时候,不论当前的内存空间是否足够,这个对象都会被回收
-
SoftReference:软件引用,基机与弱引用类似,不同在于只有当空间不足时GC才才回收软引用对象。
-
-
其他功能
- 过期清理:指清理存放数据过久的数据
- 线程安全:保证缓存可以被多个线程同时使用
- 写安全:当拿到缓存数据后,可对其进行修改,而不影响原本的缓存数据。通常采取做法是对缓存对象进行深拷贝。
5.二级缓存的设计
这么多的功能,如何才能简单的实现,并保证它的灵活性与扩展性呢?这里MyBatis抽像出Cache接口,其只定义了缓存中最基本的功能方法:
- 设置缓存
- 获取缓存
- 清除缓存
- 获取缓存数量
然后上述中每一个功能都会对应一个组件类,并基于装饰者加责任链的模式,将各个组件进行串联。在执行缓存的基本功能时,其它的缓存逻辑会沿着这个责任链依次往下传递。
这样设计有以下优点:
- 职责单一:各个节点只负责自己的逻辑,不需要关心其它节点。
- 扩展性强:可根据需要扩展节点、删除节点,还可以调换顺序保证灵活性。
- 松耦合:各节点之间不没强制依赖其它节点。而是通过顶层的Cache接口进行间接依赖。
5.1 BlockingCache
防止高速缓存未命中时对数据库的大规模访问,它设置了对高速缓存键的锁定
public class BlockingCache implements Cache {
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
// 获取值,并发情况下如果key正在获取,一直blocking,直到有对应的数据进入缓存
acquireLock(key);
Object value = delegate.getObject(key);
// 如果没有值则一直阻塞,直到putObject
if (value != null) {
releaseLock(key);
}
return value;
}
private void acquireLock(Object key) {
CountDownLatch newLatch = new CountDownLatch(1);
while (true) {
// 如果传入key对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key和value,返回null
CountDownLatch latch = locks.putIfAbsent(key, newLatch);
if (latch == null) {
break;
}
try {
if (timeout > 0) {
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);
}
}
}
}
5.2 SynchronizedCache
锁定缓存,防止并发
public class SynchronizedCache implements Cache {
private final Cache delegate;
public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}
@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
}
5.3 LoggingCache
记录缓存的命中率,以同一个mapper xml 文件所有开启二级缓存的查询为基数
public class LoggingCache implements Cache {
private final Log log;
private final Cache delegate;
protected int requests = 0;
protected int hits = 0;
public LoggingCache(Cache delegate) {
this.delegate = delegate;
this.log = LogFactory.getLog(getId());
}
@Override
public Object getObject(Object key) {
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
hits++;
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}
}
5.4 ScheduledCache
定期清除缓存
public class ScheduledCache implements Cache {
private final Cache delegate;
// mapper.xml文件中配置的flushInterval,缓存刷新间隔时间
protected long clearInterval;
protected long lastClear;
public ScheduledCache(Cache delegate) {
this.delegate = delegate;
// 默认一小时清除一次
this.clearInterval = TimeUnit.HOURS.toMillis(1);
this.lastClear = System.currentTimeMillis();
}
// 是否到时间清除缓存
private boolean clearWhenStale() {
// 到了清除缓存的时间
if (System.currentTimeMillis() - lastClear > clearInterval) {
// 清除缓存
clear();
return true;
}
return false;
}
@Override
public void clear() {
// 更新最后一次清除缓存的时间
lastClear = System.currentTimeMillis();
delegate.clear();
}
@Override
public void putObject(Object key, Object object) {
clearWhenStale();
delegate.putObject(key, object);
}
@Override
public Object getObject(Object key) {
// 到了清除缓存的时间,返回null
return clearWhenStale() ? null : delegate.getObject(key);
}
}
5.5 LruCache
缓存满了之后淘汰策略(近期最少使用),主要依赖于LinkedHashMap实现。
LinkedHashMap继承于HashMap,它使用了一个双向链表来存储 Map 中的 Entry 顺序关系,对于 get、put、remove 等操作,LinkedHashMap 除了要做 HashMap 做的事情,还做些调整 Entry 顺序链表的工作。LruCache 中将 LinkedHashMap 的顺序设置为 LRU 顺序来实现 LRU 缓存,每次调用 get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用 put 插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。
public class LruCache implements Cache {
private final Cache delegate;
// mapper.xml文件 配置的size 缓存存放多少个元素即为Map的大小
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
// mapper.xml文件 配置的size 缓存存放多少个元素
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
keyMap.get(key); // touch
return delegate.getObject(key);
}
// 每次把结果放入缓存,需要先把结果放入LinkedHashMap,当Map满了之后,需要移除近期最少使用的
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
5.6 FifoCache
先进先出策略。元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存
public class FifoCache implements Cache {
private final Cache delegate;
private final Deque<Object> keyList;
// mapper.xml文件 配置的size 缓存存放多少个元素
private int size;
public FifoCache(Cache delegate) {
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024;
}
// 设置数据量大小
public void setSize(int size) {
this.size = size;
}
@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);
}
}
}
5.7 PerpetualCache
缓存数据真正存储的地方(内存中),一级缓存已经介绍过。
6.CachingExecutor
CachingExecutor缓存执行器,用于处理二级缓存的。二级缓存和一级缓存相对独立的逻辑,而且二级缓存可以通过参数控制关闭,而一级缓存是不可以的。综上原因把二级缓存单独抽出来处理。抽取的方式采用了装饰者设计模式,即在CachingExecutor 对原有的执行器进行包装,处理完二级缓存逻辑之后,把SQL执行相关的逻辑交给实至的Executor处理。
6.1二级缓存命中条件
二级缓存的命中场景与一级缓存类似,不同在于二级可以跨会话使用,还有就是二级缓存的更新,必须是在会话提交之后。
- 相同的MapperStatement ID
- SQL语句相同
- 参数相同
- RowBounds行范围相同
- 相同的environmentId(防止切换数据库)
- 没有使用ResultHandler来自定义返回数据
- 没有配置UseCache=false 来关闭缓存
- 没有配置FlushCache=true 来清空缓存
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
// 如果需要的话刷新缓存,即FlushCache=true
flushCacheIfRequired(ms);
// 查询使用缓存UseCache=true
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 查询缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 缓存中无数据
// 委托模式,交给SimpleExecutor等实现类去实现方法。
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);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// FlushCache=true,清除缓存
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
7.二级缓存结构
二级缓存的生效,是在会话提交之后。
为什么要提交之后才能命中缓存?
二级缓存是可以跨线程会话使用的。
如上图两个会话在修改同一数据,当会话二修改后,在将其查询出来,假如它实时填充到二级缓存,而会话一就能过缓存获取修改之后的数据,但实质是修改的数据回滚了,并没真正的提交到数据库。
所以为了保证数据一至性,二级缓存必须是会话提交之后才会真正填充,包括对缓存的清空,也必须是会话正常提交之后才生效。
为了实现会话提交之后才变更二级缓存,MyBatis为每个会话设立了若干个暂存区,当前会话对指定缓存空间的变更,都存放在对应的暂存区,当会话提交之后才会提交到每个暂存区对应的缓存空间。为了统一管理这些暂存区,每个会话都有一个唯一的事物缓存管理器。
7.1TransactionalCacheManager
事物缓存管理器
public class TransactionalCacheManager {
// 缓存暂存区,一个会话可能使用多个Cache
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
// 清除缓存
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
// 获取缓存
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
// 加入缓存暂存区
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
// 暂存区缓存提至二级缓存
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
// 回滚,清除暂存区缓存内容
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
7.2TransactionalCache
缓存暂存区
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
// 具体实现的缓存
private final Cache delegate;
// 是否已经清除暂存区缓存
private boolean clearOnCommit;
// 暂存区存入的缓存值(key -> value),待commit提交至二级缓存
private final Map<Object, Object> entriesToAddOnCommit;
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
// 获取缓存
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
// 二级缓存中无值
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
// 放入缓存,加入暂存区中,等待commit刷新至二级缓存
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
// 提交,把缓存放入二级缓存
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter. "
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
}
-
查询操作query
当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。
-
更新操作update
当执行update操作时,在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。
如果在查询操作中配置了flushCache=true ,也会执行相同的操作。
-
提交操作commit
mmit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn(“Unexpected exception while notifiying a rollback to the cache adapter. ”
+ “Consider upgrading your cache adapter to the latest version. Cause: ” + e);
}
}
}
}
- 查询操作query
当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。
- 更新操作update
当执行update操作时,在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。
> 如果在查询操作中配置了flushCache=true ,也会执行相同的操作。
- 提交操作commit
当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。