返回介绍

Spring 系列

MyBatis

Netty

Dubbo

Tomcat

Redis

Nacos

Sentinel

RocketMQ

番外篇(JDK 1.8)

学习心得

缓存模块

发布于 2024-05-19 21:34:34 字数 40976 浏览 0 评论 0 收藏 0

MyBatis 中的缓存分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是 Cache 接口 的实现。MyBatis 缓存模块 的设计,使用了装饰器模式,这里不对此进行过多解析,以后会专门开一篇博文分析常用框架中使用到的设计模式。

1 Cache 组件

MyBatis 中缓存模块相关的代码位于 org.apache.ibatis.cache 包 下,其中 Cache 接口 是缓存模块中最核心的接口,它定义了所有缓存的基本行为。

public interface Cache {

  /**
   * 获取当前缓存的 Id
   */
  String getId();

  /**
   * 存入缓存的 key 和 value,key 一般为 CacheKey对象
   */
  void putObject(Object key, Object value);

  /**
   * 根据 key 获取缓存值
   */
  Object getObject(Object key);

  /**
   * 删除指定的缓存项
   */
  Object removeObject(Object key);

  /**
   * 清空缓存
   */
  void clear();

  /**
   * 获取缓存的大小
   */
  int getSize();

  /**
   * !!!!!!!!!!!!!!!!!!!!!!!!!!
   * 获取读写锁,可以看到,这个接口方法提供了默认的实现!!
   * 这是 Java8 的新特性!!只是平时开发时很少用到!!!
   * !!!!!!!!!!!!!!!!!!!!!!!!!!
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }
}

如下图所示,Cache 接口 的实现类有很多,但大部分都是装饰器,只有 PerpetualCache 提供了 Cache 接口 的基本实现。

avatar

1.1 PerpetualCache

PerpetualCache(Perpetual:永恒的,持续的)在缓存模块中扮演着被装饰的角色,其实现比较简单,底层使用 HashMap 记录缓存项,也是通过该 HashMap 对象 的方法实现的 Cache 接口 中定义的相应方法。

public class PerpetualCache implements Cache {

  // Cache对象 的唯一标识
  private final String id;

  // 其所有的缓存功能实现,都是基于 JDK 的 HashMap 提供的方法
  private Map<Object, Object> cache = new 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();
  }

  /**
   * 其重写了 Object 中的 equals() 和 hashCode()方法,两者都只关心 id字段
   */
  @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());
  }

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

下面来看一下 cache.decorators 包 下提供的装饰器,它们都直接实现了 Cache 接口,扮演着装饰器的角色。这些装饰器会在 PerpetualCache 的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求。

1.2 BlockingCache

BlockingCache 是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据。

public class BlockingCache implements Cache {

  // 阻塞超时时长
  private long timeout;
  // 持有的被装饰者
  private final Cache delegate;
  // 每个 key 都有其对应的 ReentrantLock锁对象
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

  // 初始化 持有的持有的被装饰者 和 锁集合
  public BlockingCache(Cache delegate) {
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<>();
  }
}

假设 线程 A 在 BlockingCache 中未查找到 keyA 对应的缓存项时,线程 A 会获取 keyA 对应的锁,这样,线程 A 在后续查找 keyA 时,其它线程会被阻塞。

  // 根据 key 获取锁对象,然后上锁
  private void acquireLock(Object key) {
    // 获取 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 ReentrantLock getLockForKey(Object key) {
    // Java8 新特性,Map系列类 中新增的方法
    // V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
    // 表示,若 key 对应的 value 为空,则将第二个参数的返回值存入该 Map集合 并返回
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
  }

假设 线程 A 从数据库中查找到 keyA 对应的结果对象后,将结果对象放入到 BlockingCache 中,此时 线程 A 会释放 keyA 对应的锁,唤醒阻塞在该锁上的线程。其它线程即可从 BlockingCache 中获取 keyA 对应的数据,而不是再次访问数据库。

  @Override
  public void putObject(Object key, Object value) {
    try {
      // 存入 key 和其对应的缓存项
      delegate.putObject(key, value);
    } finally {
      // 最后释放锁
      releaseLock(key);
    }
  }

  private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    // 锁是否被当前线程持有
    if (lock.isHeldByCurrentThread()) {
      // 是,则释放锁
      lock.unlock();
    }
  }

1.3 FifoCache 和 LruCache

在很多场景中,为了控制缓存的大小,系统需要按照一定的规则清理缓存。FifoCache 是先入先出版本的装饰器,当向缓存添加数据时,如果缓存项的个数已经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。

public class FifoCache implements Cache {

  // 被装饰对象
  private final Cache delegate;
  // 用一个 FIFO 的队列记录 key 的顺序,其具体实现为 LinkedList
  private final Deque<Object> keyList;
  // 决定了缓存的容量上限
  private int size;

  // 国际惯例,通过构造方法初始化自己的属性,缓存容量上限默认为 1024个
  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<>();
    this.size = 1024;
  }

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

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

  public void setSize(int size) {
    this.size = size;
  }

  @Override
  public void putObject(Object key, Object value) {
    // 存储缓存项之前,先在 keyList 中注册
    cycleKeyList(key);
    // 存储缓存项
    delegate.putObject(key, value);
  }

  private void cycleKeyList(Object key) {
    // 在 keyList队列 中注册要添加的 key
    keyList.addLast(key);
    // 如果注册这个 key 会超出容积上限,则把最老的一个缓存项清除掉
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }

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

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

  // 除了清理缓存项,还要清理 key 的注册列表
  @Override
  public void clear() {
    delegate.clear();
    keyList.clear();
  }

}

LruCache 是按照"近期最少使用算法"(Least Recently Used, LRU)进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。

public class LruCache implements Cache {

  // 被装饰者
  private final Cache delegate;
  // 这里使用的是 LinkedHashMap,它继承了 HashMap,但它的元素是有序的
  private Map<Object, Object> keyMap;
  // 最近最少被使用的缓存项的 key
  private Object eldestKey;

  // 国际惯例,构造方法中进行属性初始化
  public LruCache(Cache delegate) {
    this.delegate = delegate;
    // 这里初始化了 keyMap,并定义了 eldestKey 的取值规则
    setSize(1024);
  }

  public void setSize(final int size) {
    // 初始化 keyMap,同时指定该 Map 的初始容积及加载因子,第三个参数true 表示 该LinkedHashMap
    // 记录的顺序是 accessOrder,即,LinkedHashMap.get()方法 会改变其中元素的顺序
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      // 当调用 LinkedHashMap.put()方法 时,该方法会被调用
      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          // 当已达到缓存上限,更新 eldestKey字段,后面将其删除
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  // 存储缓存项
  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    // 记录缓存项的 key,超出容量则清除最久未使用的缓存项
    cycleKeyList(key);
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    // eldestKey 不为空,则表示已经达到缓存上限
    if (eldestKey != null) {
      // 清除最久未使用的缓存
      delegate.removeObject(eldestKey);
      // 制空
      eldestKey = null;
    }
  }

  @Override
  public Object getObject(Object key) {
    // 访问 key元素 会改变该元素在 LinkedHashMap 中的顺序
    keyMap.get(key); //touch
    return delegate.getObject(key);
  }

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

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

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

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

}

1.4 SoftCache 和 WeakCache

在分析 SoftCache 和 WeakCache 实现之前,我们再温习一下 Java 提供的 4 种引用类型,强引用 StrongReference、软引用 SoftReference、弱引用 WeakReference 和虚引用 PhantomReference。

  • 强引用 平时用的最多的,如 Object obj = new Object(),新建的 Object 对象 就是被强引用的。如果一个对象被强引用,即使是 JVM 内存空间不足,要抛出 OutOfMemoryError 异常,GC 也绝不会回收该对象。
  • 软引用 仅次于强引用的一种引用,它使用类 SoftReference 来表示。当 JVM 内存不足时,GC 会回收那些只被软引用指向的对象,从而避免内存溢出。软引用适合引用那些可以通过其他方式恢复的对象,例如, 数据库缓存中的对象就可以从数据库中恢复,所以软引用可以用来实现缓存,下面要介绍的 SoftCache 就是通过软引用实现的。
    另外,由于在程序使用软引用之前的某个时刻,其所指向的对象可能己经被 GC 回收掉了,所以通过 Reference.get()方法 来获取软引用所指向的对象时,总是要通过检查该方法返回值是否为 null,来判断被软引用的对象是否还存活。
  • 弱引用 弱引用使用 WeakReference 表示,它不会阻止所引用的对象被 GC 回收。在 JVM 进行垃圾回收时,如果指向一个对象的所有引用都是弱引用,那么该对象会被回收。 所以,只被弱引用所指向的对象,其生存周期是 两次 GC 之间 的这段时间,而只被软引用所指向的对象可以经历多次 GC,直到出现内存紧张的情况才被回收。
  • 虚引用 最弱的一种引用类型,由类 PhantomReference 表示。虚引用可以用来实现比较精细的内存使用控制,但很少使用。
  • 引用队列(ReferenceQueue ) 很多场景下,我们的程序需要在一个对象被 GC 时得到通知,引用队列就是用于收集这些信息的队列。在创建 SoftReference 对象 时,可以为其关联一个引用队列,当 SoftReference 所引用的对象被 GC 时, JVM 就会将该 SoftReference 对象 添加到与之关联的引用队列中。当需要检测这些通知信息时,就可以从引用队列中获取这些 SoftReference 对象。不仅是 SoftReference,弱引用和虚引用都可以关联相应的队列。

现在来看一下 SoftCache 的具体实现。

public class SoftCache implements Cache {

  // 这里使用了 LinkedList 作为容器,在 SoftCache 中,最近使用的一部分缓存项不会被 GC
  // 这是通过将其 value 添加到 hardLinksToAvoidGarbageCollection集合 实现的(即,有强引用指向其value)
  private final Deque<Object> hardLinksToAvoidGarbageCollection;
  // 引用队列,用于记录已经被 GC 的缓存项所对应的 SoftEntry对象
  private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
  // 持有的被装饰者
  private final Cache delegate;
  // 强连接的个数,默认为 256
  private int numberOfHardLinks;

  // 构造方法进行属性的初始化
  public SoftCache(Cache delegate) {
    this.delegate = delegate;
    this.numberOfHardLinks = 256;
    this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
    this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
  }

  private static class SoftEntry extends SoftReference<Object> {
    private final Object key;

    SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
      // 指向 value 的引用是软引用,并且关联了 引用队列
      super(value, garbageCollectionQueue);
      // 强引用
      this.key = key;
    }
  }

  @Override
  public void putObject(Object key, Object value) {
    // 清除已经被 GC 的缓存项
    removeGarbageCollectedItems();
    // 添加缓存
    delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
  }

  private void removeGarbageCollectedItems() {
    SoftEntry sv;
    // 遍历 queueOfGarbageCollectedEntries集合,清除已经被 GC 的缓存项 value
    while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
      delegate.removeObject(sv.key);
    }
  }

  @Override
  public Object getObject(Object key) {
    Object result = null;
    @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
      // 用一个软引用指向 key 对应的缓存项
      SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
    // 检测缓存中是否有对应的缓存项
    if (softReference != null) {
      // 获取 softReference 引用的 value
      result = softReference.get();
      // 如果 softReference 引用的对象已经被 GC,则从缓存中清除对应的缓存项
      if (result == null) {
        delegate.removeObject(key);
      } else {
        synchronized (hardLinksToAvoidGarbageCollection) {
          // 将缓存项的 value 添加到 hardLinksToAvoidGarbageCollection集合 中保存
          hardLinksToAvoidGarbageCollection.addFirst(result);
          // 如果 hardLinksToAvoidGarbageCollection 的容积已经超过 numberOfHardLinks
          // 则将最老的缓存项从 hardLinksToAvoidGarbageCollection 中清除,FIFO
          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
            hardLinksToAvoidGarbageCollection.removeLast();
          }
        }
      }
    }
    return result;
  }

  @Override
  public Object removeObject(Object key) {
    // 清除指定的缓存项之前,也会先清理被 GC 的缓存项
    removeGarbageCollectedItems();
    return delegate.removeObject(key);
  }


  @Override
  public void clear() {
    synchronized (hardLinksToAvoidGarbageCollection) {
      // 清理强引用集合
      hardLinksToAvoidGarbageCollection.clear();
    }
    // 清理被 GC 的缓存项
    removeGarbageCollectedItems();
    // 清理最底层的缓存项
    delegate.clear();
  }

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

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

  public void setSize(int size) {
    this.numberOfHardLinks = size;
  }

}

WeakCache 的实现与 SoftCache 基本类似,唯一的区别在于其中使用 WeakEntry(继承了 WeakReference)封装真正的 value 对象,其他实现完全一样。

另外,还有 ScheduledCache、LoggingCache、SynchronizedCache、SerializedCache 等。ScheduledCache 是周期性清理缓存的装饰器,它的 clearInterval 字段 记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear 字段 记录了最近一次清理的时间戳。 ScheduledCache 的 getObject()、putObject()、removeObject() 等核心方法,在执行时都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。

LoggingCache 在 Cache 的基础上提供了日志功能,它通过 hit 字段 和 request 字段 记录了 Cache 的命中次数和访问次数。在 LoggingCache.getObject()方法 中,会统计命中次数和访问次数 这两个指标,井按照指定的日志输出方式输出命中率。

SynchronizedCache 通过在每个方法上添加 synchronized 关键字,为 Cache 添加了同步功能,有点类似于 JDK 中 Collections 的 SynchronizedCollection 内部类。

SerializedCache 提供了将 value 对象 序列化的功能。SerializedCache 在添加缓存项时,会将 value 对应的 Java 对象 进行序列化,井将序列化后的 byte[]数组 作为 value 存入缓存 。 SerializedCache 在获取缓存项时,会将缓存项中的 byte[]数组 反序列化成 Java 对象。不使用 SerializedCache 装饰器 进行装饰的话,每次从缓存中获取同一 key 对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程,以及缓存中的对象。而使用 SerializedCache 每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。 SerializedCache 使用的序列化方式是 Java 原生序列化。

2 CacheKey

在 Cache 中唯一确定一个缓存项,需要使用缓存项的 key 进行比较,MyBatis 中因为涉及 动态 SQL 等多方面因素, 其缓存项的 key 不能仅仅通过一个 String 表示,所以 MyBatis 提供了 CacheKey 类 来表示缓存项的 key,在一个 CacheKey 对象 中可以封装多个影响缓存项的因素。 CacheKey 中可以添加多个对象,由这些对象共同确定两个 CacheKey 对象 是否相同。

public class CacheKey implements Cloneable, Serializable {

  private static final long serialVersionUID = 1146682552656046210L;

  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

  private static final int DEFAULT_MULTIPLYER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  // 参与计算hashcode,默认值DEFAULT_MULTIPLYER = 37
  private final int multiplier;
  // 当前CacheKey对象的hashcode,默认值DEFAULT_HASHCODE = 17
  private int hashcode;
  // 校验和
  private long checksum;
  private int count;

  // 由该集合中的所有元素 共同决定两个CacheKey对象是否相同,一般会使用一下四个元素
  // MappedStatement的id、查询结果集的范围参数(RowBounds的offset和limit)
  // SQL语句(其中可能包含占位符"?")、SQL语句中占位符的实际参数
  private List<Object> updateList;

  // 构造方法初始化属性
  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }

  public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }

  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    // 重新计算count、checksum和hashcode的值
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    // 将object添加到updateList集合
    updateList.add(object);
  }

  public int getUpdateCount() {
    return updateList.size();
  }

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }

  /**
   * CacheKey重写了 equals() 和 hashCode()方法,这两个方法使用上面介绍
   * 的 count、checksum、hashcode、updateList 比较两个 CacheKey对象 是否相同
   */
  @Override
  public boolean equals(Object object) {
    // 如果为同一对象,直接返回 true
    if (this == object) {
      return true;
    }
    // 如果 object 都不是 CacheKey类型,直接返回 false
    if (!(object instanceof CacheKey)) {
      return false;
    }

    // 类型转换一下
    final CacheKey cacheKey = (CacheKey) object;

    // 依次比较 hashcode、checksum、count,如果不等,直接返回 false
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }

    // 比较 updateList 中的元素是否相同,不同直接返回 false
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

  @Override
  public int hashCode() {
    return hashcode;
  }

  @Override
  public String toString() {
    StringJoiner returnValue = new StringJoiner(":");
    returnValue.add(String.valueOf(hashcode));
    returnValue.add(String.valueOf(checksum));
    updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
    return returnValue.toString();
  }

  @Override
  public CacheKey clone() throws CloneNotSupportedException {
    CacheKey clonedCacheKey = (CacheKey) super.clone();
    clonedCacheKey.updateList = new ArrayList<>(updateList);
    return clonedCacheKey;
  }

}

3 小结

至此 Mybatis 的基础支持层的主要模块就分析完了。本模块首先介绍了 MyBatis 对 Java 反射机制的封装;然后分析了类型转换 TypeHandler 组件,了解了 MyBatis 如何实现数据在 Java 类型 与 JDBC 类型 之间的转换。

之后分析了 MyBatis 提供的 DataSource 模块 的实现和原理,深入解析了 MyBatis 自带的连接池 PooledDataSource 的详细实现;后面紧接着介绍了 Transaction 模块 的功能。然后分析了 binding 模块 如何将 Mapper 接口 与映射配置信息相关联,以及其中的原理。最后介绍了 MyBatis 的缓存模块,分析了 Cache 接口 以及多个实现类的具体实现,它们是 Mybatis 中一级缓存和二级缓存的基础。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文