Mybatis源码阅读之--二级缓存实现原理分析

前言:
Mybatis为了提升性能,为每个Mapper设置了二级缓存机制,其作用域为每个Mapper,与一级缓存不同的是,一级缓存的作用域可以设置为Session级别,也可以是Statement级别,而二级缓存则是全局级别的,不同的session共用同一个二级缓存。
但是二级缓存是比较鸡肋的东西,会引发一些问题,并不推荐开启

一、二级缓存的设置与使用:

  1. 可以在config文件中xml<settings><setting name="cacheEnabled" value="true"/></settings>开启全局的二级缓存,但并不会为所有的Mapper设置二级缓存
  2. 每个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。

  1. 每个select类型的statement都可以设置useCach为false,不启用二级缓存;还有flushCache,是否需要刷新缓存(先把原来的缓存清理掉)

二、每个Mapper对应的Cache的创建过程
XMLMapperBuilder在解析各个Mapper.xml文件时,会为每个启用了二级缓存的Mapper创建一个Cache,并绑定到此mapper的每一个MappedStatement上,当然不包含那些属性useCache设置为false的Statement。
代码片段如下:

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);
  }

主要步骤有两个:

  1. 在必要的时候清空二级缓存 flushCacheIfRequired(ms)
  2. 调用底层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);
  }

查询的主要思路如下:

  1. mapper配置了二级缓存--处理二级缓存
    (注:MappedStatement中Cache对象是应用的整个mapper的cache,在解析mapper的时候,会为每个mapper分配一个Cache,而MappedStatement就是引用的此cache,若mapper开启了二级缓存,那么此mapper下的所有的statement中的cache都不为null)
  2. 在需要的时候清空缓存
  3. 如果语句使用了缓存,并且resultHandler不为空,则先进行检测,Statement类型为callable的时候没有设置出参才能使用
  4. 从二级缓存中取出数据,如果缓存中没有数据,则从一级缓存或者数据库中获取,并将数据存放至二级缓存中
  5. 如果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)包装了一层,从而实现事务处理的功能。
这里最重要的有三个属性

  1. entriesToAddOnCommit--记录了在提交时需要向底层缓存中写入哪些数据
  2. entrisMissedInCache--记录了未从缓存中获取到的数据
  3. 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;
    }
  }

主要步骤如下:

  1. 从底层缓存中取数据
  2. 如果没有取出数据,则将此key放入至entriesMissedInCache中
  3. 如果取出了数据,但是缓存已经被清楚了,那么返回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,此方法有两个主要功能:

  1. 将待添加的条目存入底层缓存中
  2. 将未命中的条目在底层缓存中存放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的操作有所迷惑:

  1. 在commit的方法中,为什么要在底层缓存中设置未命中的条目为null
  2. 在rollback方法中,为什么要在底层缓存中移除未命中的条目
  3. 在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;
  }
}

其中代码中的注释已经听清楚的了,这里再进行总结分析一下:

  1. 每个key都对应一个ReentrantLock,用来阻塞其他线程
  2. 获取数据时,先获取锁,然后取数据,如果取到了数据,则释放锁,否则不释放锁
  3. 设置数据后,会执行释放锁的操作,用以唤醒其他等待获取此缓存的线程
  4. 删除缓存时,释放锁
    以上所说的释放锁,只有在锁是当前线程拥有时,才会进行释放,而且获取锁时可以设置超时时间,用以避免线程过长时间的等待。
    因此BlockingCache的使用是需要getObject和putObject成对使用,即如果getObject拿到了null,那么后续需要进行putObject操作,否则会造成锁资源的占用,无法释放的问题。

有了这个思路,在回过头看一看TransactionalCache的实现,其实是为了适应BlockingCache所做的:

  1. delegate.getObject为null时,会向entriesMissedInCache中记录一条数据,这里底层Cache可能使用的是cache,那么就是entriesMissedInCache这些条目都还没有释放锁,后续需要释放
  2. clear的时候不清除entriesMissedInCache中的数据,因为如果清除了,有可能会导致锁没有释放的风险
  3. rollback的时候会将未命中的缓存remove掉,达到释放锁的功能
  4. commit的时候会将entriedMissedInCache中的条目向底层缓存中设置null,将锁释放掉

这里的实现方式还是有很多疑惑的地方:

  1. 为什么rollback时调用的是delegate.removeObject(key),而commit的时候为什么会调用delegate.putObject(key, null)
  2. 在BlockingCache中removeObject为什么仅仅执行了释放锁的操作,却不执行delegate.removeObject(key)

总结:

  1. 二级缓存为每个mapper创建了一个cache对象,从而到达了全局缓存的效果,即其可以作用在多个session中,session公用缓存
  2. 每个session中查询数据时,会不将数据立刻放入缓存中,而是先放进一个缓冲区中(entriesAddOnCommit),等待事务提交之后才其他session才可以使用
  3. 缓存清理时,并不会立刻清除,而是等在提交或者回滚时进行提交

二级缓存会带来的问题:

  1. 由于每个mapper中有一个缓存对象,那么如果MapperA执行A表的查询操作,二级缓存中有了数据,而MapperB执行表A的更新操作,那么不会造成MapperA的二级缓存失效,当MapperA再次查询相同的数据时,会从二级缓存中取出数据,而此时数据时脏的
  2. 当一个session中执行了清除MapperA的缓存动作,但并未提交,其他session依旧可以从缓存中拿到数据
    由于以上原因,不建议使用Mybatis的二级缓存功能,使用外部缓存会更好。

猜你喜欢

转载自www.cnblogs.com/autumnlight/p/12653562.html
今日推荐