mybatis 之缓存接口

为啥使用缓存?

这是一个值得思考清楚的问题, 为了能够加快访问速度,提高系统的吞吐量。

使用缓存带来的问题?

缓存一致性问题, 即缓存是否与最新的数据保持一致。

mybatis的缓存

mybatis作为一个ORM框架, 数据的通过JDBC从数据库读取, 读取是需要进行网络通信IO,以及磁盘IO,为了提升速度,势必有必要进行缓存到内存。就比如查询操作, 如果是对同一个缓存key进行操作, 那就没必要每次都去查数据库, 直接走缓存。

mybatis的缓存设计体系

mybatis的缓存分为一级缓存和二级缓存。 访问的时候先走二级缓存, 在走一级缓存。 一级缓存一直存在, 二级缓存默认开启, 但是需要给MapperStatement设置开启标识。

mybatis的缓存接口如下

public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();

  /**
   * @param key
   *          Can be any object but usually it is a {@link CacheKey}
   * @param value
   *          The result of a select.
   */
  void putObject(Object key, Object value);

  /**
   * @param key
   *          The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**
   * As of 3.3.0 this method is only called during a rollback
   * for any previous value that was missing in the cache.
   * This lets any blocking cache to release the lock that
   * may have previously put on the key.
   * A blocking cache puts a lock when a value is null
   * and releases it when the value is back again.
   * This way other threads will wait for the value to be
   * available instead of hitting the database.
   *
   *
   * @param key
   *          The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance.
   */
  void clear();

  /**
   * Optional. This method is not called by the core.
   *
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();

  /**
   * Optional. As of 3.2.6 this method is no longer called by the core.
   * <p>
   * Any locking needed by the cache must be provided internally by the cache provider.
   *
   * @return A ReadWriteLock
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

缓存接口实现类分析

首先得知道mybatis的缓存设置整体上使用了装饰器设计模式 也有委托者模式的影子。

PerpetualCache

缓存说到底就是把数据存储在内存里面, 那么必定会有一个数据结构用来存储的,所以这个PerpetualCache 既是缓存功能实现的基石。

public class PerpetualCache implements Cache { //持久化缓存

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>(); //使用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()); //使用id进行判断
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

关键也就是使用了HashMap作为缓存的存储结构。

LruCache

缓存, 如果没有置换算法,时不能一直往里面加的,比如我设置缓存大小为10M, 那么 但要达到10M,就必须采取换粗置换算法,把一些不值得的数据用新的覆盖掉, LruCache采用了最近最久未使用的算来淘汰, 当然还有一个比较出名的最近最少使用。 Mybatis默认提供了前者。 大家也可以想一下, 用户什么数据结构实现比较好。链表

为啥呢, 想一下缓存key一直put, 如果使用数组来维护, 数据就得一直扩容,所以使用链表比较号,不管时使用数据还是使用链表, 一个问题就遍历查找效率都比较低, 所以需要使用hash表来提升查找速度,这样我们就可以想到HashMap, 但是我们还需要记录添加的先后顺序,所以想到LinkedHashMap。

熟悉HashMap代码的, 应该都会留意HashMap的插入过程留下很多的模板方法,提供一些功能扩展。

LinkedHashMap, 最大的特点就是说期维护了一个前后插入关系的链表结构,要知道HashMap的读取的时候是和插入的时候的顺序无关。

那LinkedHashMap怎么做到呢?

static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after; //LinkedhashMap为啥有序的原因
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

上图为LinkedHashMap的Entry节点, 其继承了HashMap的Node节点。
补充了 before, after 引用, 这也就说明了LinkedHashMap如何做到有序的。

另一个问题就在那些地方维护这样一个before、after结构呢?

在HashMap的put过程中, 主要是两种情况, 第一种就是put的元素key在HashMap不存在,属于新插入的,所以在这种情况下 Before、After的结构 采用尾巴插法,维护先后关系。 第二种情况下就是 插入的key在HashMap中是存在的,根据HashMap的代码扩展模板方法, 可以看见LinkedHashMap在这种情况下是在afterNodeAccess(Node)方法中维护了 这个Before、After关系。

前面说了这么多, 那这和LRU有啥关系呢?
既然数据已经存入了,那么现在只需要把最久为使用的节点拿掉就行。
这部分逻辑在模板方法afterNodeInsertion(evict)中实现。

 void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

从图中可见first指向了 最久为使用的head节点。
其中LinkedHashMap又提供了一个removeEldestEntry方法留给子类去扩展,到底要不要移除这个最久为使用的节点。

最后回到LruCache , keyMap实现了LinkedHashMap 并重写removeEldestEntry方法, 设置了如果LinkedHashMap的数据量大小大于设置的size,那么久进行移除。 并记录这要移除的key,用来移除缓存。

private void cycleKeyList(Object key) {
    keyMap.put(key, key); //lru
    if (eldestKey != null) {//存不存在老key
      delegate.removeObject(eldestKey);//缓存移除
      eldestKey = null;
    }
  }

BlockingCache

当在缓存中找不到元素时,它在缓存键上设置一个锁。这样,其他线程将等待该元素被填满,而不是访问数据库。根据其性质,如果使用不当,此实现可能导致死锁。

public class BlockingCache implements Cache {

  private long timeout;
  private final Cache delegate;
  private final ConcurrentHashMap<Object, CountDownLatch> locks;//呃,这个也太离谱了
  }

其数据结构上述代码所示, timeout 为等待锁的超时时间, delegate为委托的另外缓存,locks 为map结构key为缓存key ,value为锁对象, 有趣的是这个锁对象使用的是CountDownLatch,

添加缓存
添加缓存, 并不控制,只能由一个线程去给当前key添加, 当缓存添加之后需要释放锁,缓存所有等待该缓存的线程。

  private void releaseLock(Object key) {
    CountDownLatch latch = locks.remove(key);//获取该缓存key的 倒计数器
    if (latch == null) { //这种情况不应该发生
      throw new IllegalStateException("Detected an attempt at releasing unacquired lock. This should never happen.");
    }
    latch.countDown(); //CountDownLatch 用法
  }

获取缓存

public Object getObject(Object key) {
    acquireLock(key);//尝试获取锁
    Object value = delegate.getObject(key);
    if (value != null) {
      releaseLock(key); //释放锁
    }
    return value;
  }

获取锁的方法

private void acquireLock(Object key) {
    CountDownLatch newLatch = new CountDownLatch(1); //创建一个倒计数器
    while (true) {
      CountDownLatch latch = locks.putIfAbsent(key, newLatch); //如果key不存在那么我就把当前的到计数器塞入 并发map, 存在那么返回原先的到计数器
      if (latch == null) { //说明我是第一个访问该key的
        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);
      }
    }
  }

上述逻辑 所有的缓存key都会公用一个CounDownLatch, 并由添加该缓存key是countDown释放锁, 但是我是感觉有个bug的就是, 第一个该key的缓存时不会被锁住的。

LoggingCache

这个就比较简单了, 一个日志缓存命中的装饰器。
数据结构为:

public class LoggingCache implements Cache { //日志缓存

  private final Log log; //日志
  private final Cache delegate;
  protected int requests = 0; //请求次数
  protected int hits = 0; //命中次数
  }

具体计算命中百分比:

@Override
  public Object getObject(Object key) {
    requests++; //请求次数加1
    final Object value = delegate.getObject(key);
    if (value != null) {
      hits++; //命中次数加一
    }
    if (log.isDebugEnabled()) { //输出日志
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
    }
    return value;
  }

FifoCache

先进先出的缓存装饰器
既然时先进先出, 那么一个比较合适的数据结构就是队列。
所以FIfoCache的数据结构如下:

public class FifoCache implements Cache {

  private final Cache delegate;
  private final Deque<Object> keyList; //队列数据结构
  private int size; //队列大小
  }

缓存淘汰算法关键代码:

private void cycleKeyList(Object key) {
    keyList.addLast(key); //队列中为末尾添加缓存key
    if (keyList.size() > size) { //如果达到了大小限制
      Object oldestKey = keyList.removeFirst(); //移除最先进入的换粗key
      delegate.removeObject(oldestKey); //移除缓存
    }
  }

整个装饰器并没有考虑线程安全问题, 所以必须来一个线程安全的装饰器。

SynchronizedCache

整个缓存装饰器, 的所有cache接口方法,在实现的时候都加了Synchronized。

ScheduledCache

这个装饰也很简单, 就是设置一个清理的间隔, 每次调用自己的方法都会去判断到了清理的时间间隔了没有,需要清理就会调用委托缓存的清理方法。

public class ScheduledCache implements Cache { //定时清理缓存

  private final Cache delegate;
  protected long clearInterval;
  protected long lastClear;
  }
private boolean clearWhenStale() {
    if (System.currentTimeMillis() - lastClear > clearInterval) {//判断是否到了清理周期
      clear();
      return true;
    }
    return false;
  }

SerializedCache

序列换缓存value的装饰器

添加缓存

  public void putObject(Object key, Object object) {
    if (object == null || object instanceof Serializable) { //判断value是否支持缓存
      delegate.putObject(key, serialize((Serializable) object));
    } else {
      throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
    }
  }

序列化方法

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

WeakCache

弱引用的缓存,当JVM进行垃圾回收的时候就会把弱引用进行垃圾回收。

数据结构:

public class WeakCache implements Cache {
  private final Deque<Object> hardLinksToAvoidGarbageCollection; //强引用避免垃圾回收
  private final ReferenceQueue<Object> queueOfGarbageCollectedEntries; //引用队列
  private final Cache delegate;
  private int numberOfHardLinks; //强引用数量限制
}

进行putObject

  @Override
  public void putObject(Object key, Object value) {
    removeGarbageCollectedItems(); //清理弱引用队列里面的元素
    delegate.putObject(key, new WeakEntry(key, value, queueOfGarbageCollectedEntries)); //添加弱引用, 并指定弱引用队列
  }

getObject

@Override
  public Object getObject(Object key) {
    Object result = null;
    @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
    WeakReference<Object> weakReference = (WeakReference<Object>) delegate.getObject(key);
    if (weakReference != null) {
      result = weakReference.get(); //说明已经进行一次垃圾回收, 已经放入引用队列
      if (result == null) {
        delegate.removeObject(key); //移除
      } else { //次缓存被外交获取, 应该是比较重要的缓存,故添加强引用,避免垃圾回收
        synchronized (hardLinksToAvoidGarbageCollection) {
          hardLinksToAvoidGarbageCollection.addFirst(result);
          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { //FIFO 缓存淘汰
            hardLinksToAvoidGarbageCollection.removeLast();
          }
        }
      }
    }
    return result;
  }

SoftCache

软引用缓存装饰器 , 和弱引用装饰器差不多,区别就是在于,软引用时FULLGC时候才会把引用对象移动到引用队列。

总结

简要的介绍了 , Mybatis缓存组件, 我们可以自己实现缓存Cache接口, 然后让Mybatis使用我们的缓存实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值