Mybatis源码阅读之--二级缓存实现原理分析
前言:
Mybatis为了提升性能,为每个Mapper设置了二级缓存机制,其作用域为每个Mapper,与一级缓存不同的是,一级缓存的作用域可以设置为Session级别,也可以是Statement级别,而二级缓存则是全局级别的,不同的session共用同一个二级缓存。
但是二级缓存是比较鸡肋的东西,会引发一些问题,并不推荐开启
一、二级缓存的设置与使用:
- 可以在config文件中
xml<settings><setting name="cacheEnabled" value="true"/></settings>
开启全局的二级缓存,但并不会为所有的Mapper设置二级缓存 - 每个mapper.xml文件中使用
标签来开启当前mapper的二级缓存。
<cache
eviction="FIFO"
flushInterval="60000" <!-- 刷新间隔,也就是 -->
size="512"
readOnly="true"/>
<!-- 引用其他mapper的二级缓存 -->
<cache-ref namespace="com.xxx.Blog"/>
eviction指定淘汰策略有以下四种
1. LRU 最近最少使用--默认的淘汰策略
2. FIFO 先进先出
3. SOFT 软引用的形式(内存不够了,垃圾收集器会将仅有SoftReferece引用的对象进行回收--参见SoftReference的相关定义)
4. WEAK 弱引用的形式(不会影响垃圾收集器对仅被WeakRefernce引用的对象进行回收--参见WeakReference的相关定义)
flushInterval为刷新间隔,即过多久需要把缓存清空
size 缓存最大容量
readOnly 是否为只读,如果是true的话,那么就会缓存对象,如果是false的话,那么会把缓存的对象先序列化到ByteArray中,每次取缓存,都会从ByteArray中进行反序列化生成新的对象,因此如果readOnly设置为了false,需要保证待缓存的对象都实现了Serializable接口,或者Externalizable。
- 每个select类型的statement都可以设置useCach为false,不启用二级缓存;还有flushCache,是否需要刷新缓存(先把原来的缓存清理掉)
二、每个Mapper对应的Cache的创建过程
XMLMapperBuilder在解析各个Mapper.xml文件时,会为每个启用了二级缓存的Mapper创建一个Cache,并绑定到此mapper的每一个MappedStatement上。
代码片段如下:
private void cacheElement(XNode context) {
if (context != null) {
// 底层缓存类型,默认为PERPETUAL,用户可以自行定义自己的Cache
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对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
其中builderAssistant.useNewCache方法如下:
// 代码位于MapperBuilderAssistant类中
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 这里使用了构造器模式
// 构造一个cache十分复杂,将cache的构造和表示进行分离
Cache cache = new CacheBuilder(currentNamespace)
// 缓存默认使用PerpetualCache
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
// 淘汰策略默认使用LRU方式
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
上述方法主要使用了构造器模式对Cache进行构造,重点关注build();
// 代码位于CacheBuilder类
public Cache build() {
setDefaultImplementations();
// 创建底层Cache实例,底层Cache有个Id
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
// 自定义的cache不再进行任何的装饰
if (PerpetualCache.class.equals(cache.getClass())) {
// 配置的包装器(淘汰策略的包装器就在decorators中)
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;
}
这里的Cache创建使用了装饰者模式,先是创建了最底层的Cache对象(默认为PerpetualCache),之后在此对象的基础上一层一层的包装,本来底层的Cache功能很少,这样进行包装之后使得Cache功能更加强大,可以有淘汰策略,也可以有BlockingCache的功能,以及日志记录的功能。
包装的过程方法为setStandardDecorators(cache),如下:
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 当clearInterval设置了值时,就用ScheduledCache进行装饰
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// readOnly设置为false时,使用SerializedCache进行装饰
if (readWrite) {
cache = new SerializedCache(cache);
}
// 使用LoggingCache进行装饰
cache = new LoggingCache(cache);
// 使用SynchronizedCache进行装饰
cache = new SynchronizedCache(cache);
// 如果blocking属性设置为true时,使用BlockingCache进行装饰
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
cache包装顺序:
PerpetualCache -> LRUCache/FIFOCache/SoftCache/WeakCache -> ScheduledCache(clearInterval设置了值) -> SerializeCache(readOnly设置为false) -> LoggingCache -> SynchronizedCache -> BlockingCache(blocking设置为true)
各个Cache里面的内容挺有意思的,感兴趣的话可以进一步阅读
其中最简单的为SynchronizedCache,就是为每个方法添加了synchronized关键字进行并发访问控制。
这里对LruCache的实现进行一下说明
LRUCache
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
// 大小默认设置为1024
setSize(1024);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
// 重新设置了大小的话,新建一个LikedHashMap
// LinkedHashMap可以很好地支持LRU算法
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);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
此类中主要是使用LinkedHashMap按照访问key的访问顺序进行了排序,
每次向LinkedHashMap中添加数据时,会调用removeEldestEntry的方法用来判断是否需要移除最老的数据,
这里重写了此方法,当添加数据时,若数据超过了容量,则删除,并把最旧的key保存,以便删除缓存中的数据。
另外,每次获取缓存时,先要在keyMap中get一下,注释中是touch,也就是保证keyMap的访问顺序。
对于FIFOCache以及WeakCache和其他的一些缓存,由于篇幅问题,就不再一一介绍了。
三、二级缓存的执行原理
当mybatis配置了启用二级缓存时,executor的创建就会有所不同,具体体现在Configuration.newExecutor方法。
// Configuration类
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);
}
// 开启了二级缓存,则创建CachingExecutor并对基础的Executor进行包装
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 将所有的interceptor包含在executor中
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
从以上代码中可以发现,若开启了二级缓存,会创建一个CachingExecutor对原有的Executor进行封装。这里也使用了包装器模式,CachingExecutor对基础的Executor进行包装,从而增加二级缓存的功能。
下面分析CachingExecutor的实现:
public class CachingExecutor implements Executor {
// 被包装的Executor
private final Executor delegate;
// tcm作为二级缓存的核心
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}
在CachingExecutor中有两个属性,一个是delegate,被包装的Executor,另一个是tcm,类型为TransactionalCacheManager,从名称来看是一个拥有事务功能的缓存管理器,这个就是用来管理二级缓存的。
此类中关于二级缓存的方法有两个,其中一个是更新操作(广义更新,包含增删该),另一个是查询操作,下面一一分析。
更新操作:
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
主要步骤有两个:
- 在必要的时候清空二级缓存 flushCacheIfRequired(ms)
- 调用底层executor执行update操作 delegate.update(ms, parameterObject)
其中第一步的代码如下,主要思想就是当前语句启用了二级缓存(1.mapper配置了cache 2.statement的useCache为true),并且要需要清空缓存flushCache设置为true
这里多说一句,对于select标签,默认useCache为true,而update/insert/delete没有userCache属性可以设置
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 如果使用了缓存,并且需要清空缓存
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
其实更新操作对于二级缓存的处理逻辑很简单。主要在于查询逻辑。
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) { // mapper配置了二级缓存
// 查看是否需要清除cache,每个语句可以设置 flushCache属性,true或false,select语句默认为false,update和insert语句默认为true
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) { // 如果有resultHandler,则不能缓存
// 确保语句中没有出参
ensureNoOutParams(ms, boundSql);
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;
}
}
// 不能使用二级缓存的情况
// 1. mapper中没有配置<cache/>或<cache-ref/>
// 2. statement的useCache设置为false
// 3. resultHandler存在
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
查询的主要思路如下:
- mapper配置了二级缓存--处理二级缓存
(注:MappedStatement中Cache对象是应用的整个mapper的cache,在解析mapper的时候,会为每个mapper分配一个Cache,而MappedStatement就是引用的此cache,若mapper开启了二级缓存,那么此mapper下的所有的statement中的cache都不为null) - 在需要的时候清空缓存
- 如果语句使用了缓存,并且resultHandler不为空,则先进行检测,Statement类型为callable的时候没有设置出参才能使用
- 从二级缓存中取出数据,如果缓存中没有数据,则从一级缓存或者数据库中获取,并将数据存放至二级缓存中
- 如果mapper没有配置二级缓存,或者statement的useCache为false,亦或者此查询使用了resultHandler,那么直接从一级缓存或者数据库中获取数据
当然,若resultHandler存在,那么就不会使用一级缓存,而是直接从数据库中获取
另外在事务进行提交和回滚时,二级缓存也需要相应的提交和回滚
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
public void rollback(boolean required) throws SQLException {
try {
delegate.rollback(required);
} finally {
if (required) {
tcm.rollback();
}
}
}
CachingExecutor分析就到这里,里面的transactionalCacheMananger的实现还是挺简单的,我们接下来分析此类。
public class TransactionalCacheManager {
// key: 每个mapper的cache对象,value: 当前的事务性缓存
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();
}
}
// 如果当前的Cache没有对应的TransactionalCache,那么使用new创建一个,参数为cache
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
此类的设计思想就是每个mapper的cache对应一个TransactionalCache,由于一个sqlsession会查询多个mapper,因此相当于为每个mapper创建了一个TransactionalCache对象。
同时此类添加了提交和回滚的功能。
TransactionalCache负责完成当前事务一个mapper的二级缓存功能,其实现原理很有意思,接下来分析。
下面列出了TransactionalCache的主要所有属性,与注释:
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
// 被包装的底层缓存--mapper的cache对象
private final Cache delegate;
// 是否在提交时清空缓存
private boolean clearOnCommit;
// 记录在提交时待添加到缓存的条目
private final Map<Object, Object> entriesToAddOnCommit;
// 缓存中缺失的条目
private final Set<Object> entriesMissedInCache;
}
此类也运用到了装饰者模式,在底层的Cache上(这里所说的底层Cache即指mapper的cache)包装了一层,从而实现事务处理的功能。
这里最重要的有三个属性
- entriesToAddOnCommit--记录了在提交时需要向底层缓存中写入哪些数据
- entrisMissedInCache--记录了未从缓存中获取到的数据
- clearOnCommit--bool类型的标志,表示是否在提交时需要清空缓存
putObject的实现如下:
public void putObject(Object key, Object object) {
// 提交之前不会放入缓存中,因此在提交之前不能从二级缓存中拿到
entriesToAddOnCommit.put(key, object);
}
putObject只会将缓存放置到待添加的条目中,不会真正的放入缓存中,也就是如果当前session查了一个数据,另一个session查相同的数据,那么本次查询不能从二级缓存中拿到,只能去数据库中重新获取
getObject的实现如下:
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
// 如果用到了BlockingCache的话,这里没有命中缓存不会释放锁,要注意锁的释放问题
// tips: 缓存命中的话就不会继续持有锁
// 锁的释放时机:
// 1.事务提交,缓存也提交
// 2.事务回滚,缓存也回滚
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) { // 缓存清理了,这里就拿不到了
return null;
} else {
return object;
}
}
主要步骤如下:
- 从底层缓存中取数据
- 如果没有取出数据,则将此key放入至entriesMissedInCache中
- 如果取出了数据,但是缓存已经被清楚了,那么返回null,否则返回从缓存中获取的对象
这里clearOnCommit的设置在clear方法中:
public void clear() {
// 设置这个标志位,以便在commit的时候执行实际缓存的清除动作
// 这里并不会真正的清除
clearOnCommit = true;
entriesToAddOnCommit.clear();
// 为什么不清除 entriesMissedInCache
// 一种解释:
// 如果使用了BlockingCache,未命中的缓存还需要等待释放锁
// 这里如果清除了就不能在恰当的时刻释放那些被占用的锁
// 恰当的时刻是指:1.事务回滚 2.事务提交
}
clear方法并不会调用delegate.clear,也就是不会实际清除缓存,而是设置了clearOnCommit为true,即在提交(回滚)的时候执行清除的动作,并清除当前待添加到缓存的条目。
试想一下:session1执行清除缓存的动作但还未提交,session2还是可以拿到缓存的数据,但是session1就拿不到了,参见getObject的实现
下面分析当事务提交和回滚时,该缓存的动作:
public void commit() {
if (clearOnCommit) { // 要在提交的时候进行实际的清除动作
delegate.clear();
}
// 这里可以解释为什么事务提交之后二级缓存才可见
flushPendingEntries();
// 事务提交之后要重置此缓存,以便下次事务再次使用
reset();
}
// 将待加入缓存的条例放入缓存中
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 这里为什么要把未命中的缓存记录到实际缓存中
// 一种解释:如果缓存使用了BlockingCache,未命中的条目依然会保留锁,这里执行添加缓存会将锁释放
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
提交事务时,如果当前session执行了清除缓存的动作, 那么就执行底层的实际清楚动作delegate.clear,然后调用flushPendingEntries,此方法有两个主要功能:
- 将待添加的条目存入底层缓存中
- 将未命中的条目在底层缓存中存放null值
调用完flushPendingEntries方法之后,执行缓存的重置操作reset,即重置所有的数据
回滚事务:
public void rollback() {
unlockMissedEntries();
reset();
}
// 目前只在事务回滚的时候进行调用,如果使用了BlockingCache,那么未命中的条目依然保留了锁
// 这里执行delegate.removeObject()操作可以释放锁资源
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
事务回滚时,会将所有未命中的缓存进行移除,并重置缓存。
在上面的分析中,很多人(包括我)可能对于entriesMissedInCache的操作有所迷惑:
- 在commit的方法中,为什么要在底层缓存中设置未命中的条目为null
- 在rollback方法中,为什么要在底层缓存中移除未命中的条目
- 在clear的方法中,为什么不执行entriesMissedInCache操作
以上三个疑问都是因为不了解BlockingCache的实现。
上文中我们说了,mapper的cache会从PerpetualCache,如果配置了blocking=true的选项,会使用BlockingCache进行封装。
BlockingCache顾名思义,阻塞Cache,主要的功能就是获取缓存时,如果已有其他线程从缓存中获取,但是未取到,另一个线程再获取时,会阻塞等待其他线程设置数据。
public class BlockingCache implements Cache {
private long timeout;
private final Cache delegate;
// 每一个CacheKey都有一个ReentrantLock
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
// 当前线程进行put()操作肯定是get()操作没有拿到数据
// 并且依旧保持锁,在这里既然对象已经设置了缓存,就把锁释放掉
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
acquireLock(key);
Object value = delegate.getObject(key);
if (value != null) { // 从缓存中获取到了对象,才释放锁
releaseLock(key);
}
// 如果没有获取到对象,则依旧持有锁
// 当前线程再次put()时会释放锁
// 这样做便于阻塞其他线程同时进行获取对象,并存入缓存(即防止缓存穿透)
return value;
}
@Override
public Object removeObject(Object key) {
// despite of its name, this method is called only to release
// TODO 仅释放锁,为什么不执行clear的动作?
releaseLock(key);
return null;
}
@Override
public void clear() {
delegate.clear();
}
// 这种方式在mybatis中很多地方都用到了
private ReentrantLock getLockForKey(Object key) {
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
private void acquireLock(Object key) {
Lock lock = getLockForKey(key);
if (timeout > 0) {
try {
// 设置获取锁的超时时间
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {
lock.lock();
}
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
}
其中代码中的注释已经听清楚的了,这里再进行总结分析一下:
- 每个key都对应一个ReentrantLock,用来阻塞其他线程
- 获取数据时,先获取锁,然后取数据,如果取到了数据,则释放锁,否则不释放锁
- 设置数据后,会执行释放锁的操作,用以唤醒其他等待获取此缓存的线程
- 删除缓存时,释放锁
以上所说的释放锁,只有在锁是当前线程拥有时,才会进行释放,而且获取锁时可以设置超时时间,用以避免线程过长时间的等待。
因此BlockingCache的使用是需要getObject和putObject成对使用,即如果getObject拿到了null,那么后续需要进行putObject操作,否则会造成锁资源的占用,无法释放的问题。
有了这个思路,在回过头看一看TransactionalCache的实现,其实是为了适应BlockingCache所做的:
- delegate.getObject为null时,会向entriesMissedInCache中记录一条数据,这里底层Cache可能使用的是cache,那么就是entriesMissedInCache这些条目都还没有释放锁,后续需要释放
- clear的时候不清除entriesMissedInCache中的数据,因为如果清除了,有可能会导致锁没有释放的风险
- rollback的时候会将未命中的缓存remove掉,达到释放锁的功能
- commit的时候会将entriedMissedInCache中的条目向底层缓存中设置null,将锁释放掉
这里的实现方式还是有很多疑惑的地方:
- 为什么rollback时调用的是delegate.removeObject(key),而commit的时候为什么会调用delegate.putObject(key, null)
- 在BlockingCache中removeObject为什么仅仅执行了释放锁的操作,却不执行delegate.removeObject(key)
总结:
- 二级缓存为每个mapper创建了一个cache对象,从而到达了全局缓存的效果,即其可以作用在多个session中,session公用缓存
- 每个session中查询数据时,会不将数据立刻放入缓存中,而是先放进一个缓冲区中(entriesAddOnCommit),等待事务提交之后才其他session才可以使用
- 缓存清理时,并不会立刻清除,而是等在提交或者回滚时进行提交
二级缓存会带来的问题:
- 由于每个mapper中有一个缓存对象,那么如果MapperA执行A表的查询操作,二级缓存中有了数据,而MapperB执行表A的更新操作,那么不会造成MapperA的二级缓存失效,当MapperA再次查询相同的数据时,会从二级缓存中取出数据,而此时数据时脏的
- 当一个session中执行了清除MapperA的缓存动作,但并未提交,其他session依旧可以从缓存中拿到数据
由于以上原因,不建议使用Mybatis的二级缓存功能,使用外部缓存会更好。