写于:2020-01-12 08:52:37

为了减轻数据库的访问压力,Mybatis 在“核心处理层” 提供了 “一级缓存” 和 “二级缓存”的功能。 针对二级缓存 Mybatis 在“基础支持层” 提供了多种缓存算法的支持。

# 基础支持层-缓存支持

基础支持层-缓存支持_类图

Mybatis “基础支持层” 提供了基础缓存支持,通过 装饰者模式 为缓存赋予各种清除策略,同步安全等附加功能。

几种常见的缓存清除策略

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

# Mybatis 缓存基础定义:Cache

直接来看 Cache 类图结构

Cache类图

Mybatis 对于 Cache 的定义:

  • 缓存需要有一个唯一标识:ID 【该 id 等于 Mapper.xml 的 namespace】
  • 提供缓存增、删、改、查 的能力

# 基础缓存支持:PerpetualCache

PerpetualCache 是缓存的最基础实现,如:带有清除策略的缓存一般是以此基础缓存类为被装饰者进行扩展实现的。

直接来看代码:

PerpetualCache源码

从代码上能够知道缓存容器实际上就是一个 HashMap 容器,而缓存的增删改查,就是对 Map 的操作。

# LRU 缓存清除策略的实现:LruCache

Mybatis 使用装饰者模式 在其他 Cache 缓存上实现了支持 LRU 清除策略的缓存。

直接上代码

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) {
    // 初始化 keyMap,注意,keyMap 的类型继承自 LinkedHashMap,
    // 并覆盖了 removeEldestEntry 方法
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      // 覆盖 LinkedHashMap 的 removeEldestEntry 方法
      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) {
    // 刷新 key 在 keyMap 中的位置
    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();
  }

  @Override
  public ReadWriteLock getReadWriteLock() { return null; }

  private void cycleKeyList(Object key) {
    // 存储 key 到 keyMap 中
    keyMap.put(key, key);
    if (eldestKey != null) {
      // 从被装饰类中移除相应的缓存项
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }
}

从上述代码能够得到两个关键点:

  • 缓存默认支持 1024 的存储大小
  • LRU 清除策略是通过 LinkedHashMap 实现的。
  • 真正的缓存存在于持有的 Cache(无论装饰多少层,最后一层都是基础实现 PerpetualCache) 引用中的 HashMap 容器中,而 LinkedHashMap 只是为了实现 LRU 算法而存在的容器。

# FIFO 缓存清除策略的实现 :

Mybatis 使用装饰者模式 在其他 Cache 缓存上实现了支持 FIFO 清除策略的缓存。

直接上代码

```java
public class FifoCache implements Cache {

  private final Cache delegate;
  private Deque<Object> keyList;
  private int size;

  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<Object>();
    this.size = 1024;
  }

  @Override
  public void putObject(Object key, Object value) {
    cycleKeyList(key);
    delegate.putObject(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    // 清除所有
    delegate.clear();
    keyList.clear();
  }

  private void cycleKeyList(Object key) {
    // 当队列大小大于缓存预设值 1024 时候,移除最先填充的缓存数据
    keyList.addLast(key);
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }
}

通过代码能够得到以下几点

  • 1、FIFO 策略的实现依赖于 Deque,而 Deque 是一个双向队列。
  • 2、缓存大小为:1024
  • 3、真正的缓存存在于持有的 Cache(基础实现 PerpetualCache) 引用中的 HashMap 容器中,而 Deque 只是为了实现 FIFO 算法而存在的容器。

# 支持线程安全的缓存:SynchronizedCache

Mybatis 使用装饰者模式 在其他 Cache 缓存上实现了支持线程安全的缓存实现

直接上代码

public class SynchronizedCache implements Cache {

  private Cache delegate;
  
  public SynchronizedCache(Cache delegate) {
    this.delegate = delegate;
  }

  @Override
  public String getId() { return delegate.getId(); }

  @Override
  public synchronized int getSize() { return delegate.getSize(); }

  @Override
  public synchronized void putObject(Object key, Object object) { delegate.putObject(key, object); }

  @Override
  public synchronized Object getObject(Object key) { return delegate.getObject(key); }

  @Override
  public synchronized Object removeObject(Object key) { return delegate.removeObject(key); }

  @Override
  public synchronized void clear() { delegate.clear(); }

  @Override
  public int hashCode() { return delegate.hashCode(); }

  @Override
  public boolean equals(Object obj) { return delegate.equals(obj); }

}

通过代码能够知道: 真正实现缓存功能的是被装饰的缓存类,而 SynchronizedCache 在所有关于缓存的操作上加了 synchronized 关键字来保证缓存的线程安全。

# 支持周期性刷新的缓存:ScheduledCache

Mybatis 使用装饰者模式 在其他 Cache 缓存上实现了支持线程安全的缓存实现

代码

public class ScheduledCache implements Cache {

  /** 被装饰 Cache **/
  private final Cache delegate;
  /** 多长时间定时清除:默认 1小时 **/
  protected long clearInterval;
  /** 最后一次清除缓存时间戳 **/
  protected long lastClear;

  public ScheduledCache(Cache delegate) {
    this.delegate = delegate;
    this.clearInterval = 60 * 60 * 1000; // 1 hour
    this.lastClear = System.currentTimeMillis();
  }

  ......
  @Override
  public void putObject(Object key, Object object) {
    clearWhenStale(); // 距离上次清除时间达到 1小时,清除所有的缓存
    delegate.putObject(key, object);
  }
  ......
  /** 距离上次清除时间达到 1小时,清除所有的缓存 **/
  private boolean clearWhenStale() {
    if (System.currentTimeMillis() - lastClear > clearInterval) {
      clear();
      return true;
    }
    return false;
  }

}

ScheduledCache 所有对缓存的增删改查都会调用 ScheduledCache#clearWhenStale。 在 ScheduledCache 中维护有两个字段:

  • clearInterval 缓存需要保留的时长
  • lastClear 上一次清除缓存的时间

当距离上一次缓存清除已经过了 clearInterval 之后,则清除所有的缓存。

手动触发周期更新

# 更多缓存策略自行解析

# 二级缓存使用

当开启二级缓存时,默认使用 LRU 算法。开启二级缓存只需要在 Mapper.xml 文件中追加 <cache> 标签即可。

通过 <cache> 标签可以进行二级缓存的配置,如下:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

更多二级缓存配置,参考官方文档

小贴士:二级缓存在进行更新时力度过大,直接清除所有的缓存。参考代码:org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired 增删改,调用的都是 Executor#update ,调用时会先调用缓存清除。

精彩内容推送,请关注公众号!
最近更新时间: 3/24/2020, 9:44:42 PM