原创

MyBatis源码解析 - 缓存模块

前言

MyBatis 中的缓存是两层结构的,分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是Cache接口的实现。对应的包在org.apache.ibatis.cache下。

Cache接口及其实现

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

public interface Cache {

  String getId();   //该缓存对象的id
  //向缓存中添加数据,一般情况下key是CacheKey,value是查询结果
  void putObject(Object key, Object value);

  Object getObject(Object key); //根据指定的key,在缓存中查找对应的结果

  Object removeObject(Object key);  //删除key对应的缓存项

  void clear();     //清空缓存


  int getSize();  // 缓存项的个数,该方法不会被MyBatis核心代码使用,所以可提供空实现

  //获取读写锁,该方法不会被MyBatis核心代码使用,所以可提供空实现
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

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

public class PerpetualCache implements Cache {

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

  //用于记录缓存项的Map对象
  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  //下面所有的方法都是通过cache字段记录这个HashMap对象的相应方法实现的
  @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();
  }

  // 重写了equals()方法和hashCode()方法, 両者都只美心id宇段,并不美心cache宇段(略)
  @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接口,扮演着ConcreteDecorator的角色。这些装饰器会在PerpetualCache的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求,后面介绍二级缓存时,会见到这些装饰器是如何完成动态组合的。

BlockingCache

BlockingCache是阻塞版本的缓存装饰器,他会保证只有一个线程到数据库中查询指定key对应的数据,BlockingCache中各个字段的含义如下:

private long timeout;         //阻塞超时时间
private final Cache delegate;   //被装饰的底层Cache对象
private final ConcurrentHashMap<Object, ReentrantLock> locks; //每个key都有对应的ReentrantLock对象

假设线程A在BlockingCache中未查找到keyA对应的缓存项时,线程A会获keyA对应的锁,这样后续线程在查找keyA时会发生阻塞,如图所示。

BlockingCache.getObject()方法的代码如下:

public Object getObject(Object key) {
  acquireLock(key);       //获取该key对应的锁
  Object value = delegate.getObject(key); //查询key
  if (value != null) {
    releaseLock(key);   //缓存有key对应的缓存项,释放锁,否则继续持有锁
  }
  return value;
}

acquireLock()方法中会尝试获取指定key对应的锁。如果该key没有对应的锁对象则为其创建新的ReentrantLock对象,再加锁;如果获取锁失败,则阻塞一段时间。 acquireLock()方法的实现如下:

private void acquireLock(Object key) {
  Lock lock = getLockForKey(key);   //获取ReentrantLock对象
  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();    //获取锁 不带超时时间
  }
}
//下面是getLockForKey()方法的实现:
private ReentrantLock getLockForKey(Object key) {
    //尝试添加到locks集合中 如果存在就使用存在的对象 如果不存在就创建一个ReentrantLock对象
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
}

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

BlockingCache.putObject()方法实现如下:

public void putObject(Object key, Object value) {
  try {
    delegate.putObject(key, value); //向缓存中添加缓存项
  } finally {
    releaseLock(key); //释放锁
  }
}
//下面是releaseLock()方法的实现:
 private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) { //锁是否被当前线程持有
      lock.unlock();    //释放锁
    }
 }

FifoCache & LruCache

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

FifoCache中各个字段的含义如下:

private final Cache delegate;   //底层被装饰的Cache对象
//用于记录key进入缓存的先后顺序,使用的是LinkedList<Object>类型的集合对象
private final Deque<Object> keyList;
private int size; //记录了缓存项的,上限,超过该值,则需要清理最老的缓存项

FifoCache.getObject()removeObject()方法的实现都是直接调用底层Cache对象的对应方法,不再赘述。在FifoCache.putObject()方法中会完成缓存项个数的检测以及缓存的清理操作,具体实现如下:

public void putObject(Object key, Object value) {
  cycleKeyList(key);  //检测并清理缓存
  delegate.putObject(key, value); //添加缓存项
}
private void cycleKeyList(Object key) {
  keyList.addLast(key);   //记录key
  if (keyList.size() > size) {  //如果打到缓存上限,则清理最老的缓存项
    Object oldestKey = keyList.removeFirst();
    delegate.removeObject(oldestKey);
  }
}

LruCache是按照近期最少使用算法(Least Recently Used,LRU)进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。LruCache中定义的各个字段的含义如下:

private final Cache delegate;   //被装饰的底层Cache对象
// LinkedHashMap<Object, object>类型对象,它是一一个有序的HashMap,用于记录key最近的使用情况
private Map<Object, Object> keyMap;
private Object eldestKey; //记录最少被使用的缓存项的key

LruCache的构造函数中默认设置的缓存大小是1024,我们可以通过其setSize()方法重新设置缓存大小,具体实现如下:

public void setSize(final int size) { //重新设置缓存大小时,会重置keyMap字段
  //注意LinkedHashMap构造函数的第三个参数,true表示该LinkedHashMap记录的顺序是
  // access-order, 也就是说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;
    }
  };
}

LruCache.getObject()方法除了返回缓存项,还会调用keyMap.get()方法修改key的顺序,表示指定的key最近被使用。LruCache.putObject()方法除了添加缓存项,还会将eldestKey字段指定的缓存项清除掉。具体实现如下:

public void putObject(Object key, Object value) {
  delegate.putObject(key, value); //添加缓存项
  cycleKeyList(key);        //剔除最久未使用的缓存项
}

public Object getObject(Object key) {
  keyMap.get(key);  //修改LinkedHashMap中记录的顺序
  return delegate.getObject(key); //剔除最久未使用的缓存项
}
private void cycleKeyList(Object key) {
  keyMap.put(key, key);
  if (eldestKey != null) {  //eldestKey不为空,标识已经达到缓存上限
    delegate.removeObject(eldestKey); //剔除最久未使用的缓存项
    eldestKey = null;
  }
}

SoftCache & WeakCache

在开始介绍SoftCacheWeakCache实现之前,先给读者简单介绍一下Java 提供的4种引用类型,它们分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和幽灵引用(Phantom Reference)。相对这些引用更深入理解的话,可以参考这篇博客。Java四种引用类型

SoftCache各个字段的含义:

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

SoftCache中缓存项的valueSoftEntry对象,SoftEntry继承了SoftReference, 其中指向key的引用是强引用,而指向value的引用是软引用。SoftEntry的实现如下:

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

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

SoftCache.putObject()方法除了向缓存中添加缓存项,还会清除已经被GC回收的缓存项,其具体实现如下:

public void putObject(Object key, Object value) {
  removeGarbageCollectedItems();  //清除已经被GC回收的缓存项
  //向缓存中添加缓存项
  delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
//下面是方法removeGarbageCollectedItems()实现:
private void removeGarbageCollectedItems() {
  SoftEntry sv;
  //遍历queueOfGarbageCollectedEntries集合
  while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
    //将已经被GC回收的value对象对应的缓存项清除
    delegate.removeObject(sv.key);
  }
}

SoftCache.getObject()方法除了从缓存中查找对应的value,处理被GC回收的value对应的缓存项,还会更新hardLinksToAvoidGarbageCollection集合,具体实现如下:

public Object getObject(Object key) {
  Object result = null;
  @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
  //从缓存中查询缓存项
  SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
  if (softReference != null) {  //检测缓存中是否有缓存项
    result = softReference.get(); //获取SoftReference引用的value
    if (result == null) {     //已经被GC回收
      delegate.removeObject(key);
    } else {
      //未被GC回收
      // See #586 (and #335) modifications need more than a read lock
      synchronized (hardLinksToAvoidGarbageCollection) {
        //缓存项中的value添加到hardLinksToAvoidGarbageCollection集合
        hardLinksToAvoidGarbageCollection.addFirst(result);
        //超过numberOfHardLinks则将最老的缓存项从集合中清除 有点类似先进先出队列
        if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
          hardLinksToAvoidGarbageCollection.removeLast();
        }
      }
    }
  }
  return result;
}

SoftCache.removeObject()方法在清除缓存项之前,也会调用removeGarbageCollectedItems()方法清理被GC回收的缓存项,代码比较简单,不再贴出来了。

SoftCache.clear()方法首先清理hardLinksToAvoidGarbageCollection集合,然后清理被GC回收的缓存项,最后清理底层delegate缓存中的缓存项,具体实现如下:

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

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

ScheduledCache & LoggingCache&Synchronized & CacheSerializedCache

ScheduledCache是周期性清理缓存的装饰器,它的clearInterval字段记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear字段记录了最近一次清 理的时间戳。ScheduledCachegetObject()putObject()removeObject()等核心方法在执行时,都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。
LoggingCacheCache的基础上提供了日志功能,它通过hit字段和request字段记录了Cache的命中次数和访问次数。在LoggingCache. getObject()方法中会统计命中次数和访问次数这两个指标,并按照指定的日志输出方式输出命中率。LoggingCache代码比较简单,请读者参考代码学习。
SynchronizedCache通过在每个方法上添加synchronized关键字,为Cache添加了同步功能,有点类似于JDKCollections中的SynchronizedCollection内部类的实现。SynchronizedCache代码比较简单,请读者参考代码学习。:

SerializedCache提供了将value对象序列化的功能。SerializedCache在添加缓存项时,会将value对应的Java对象进行序列化,并将序列化后的byte[]数组作为value存入缓存。
SerializedCache在获取缓存项时,会将缓存项中的byte[]数组反序列化成Java对象。使用前面介绍的Cache装饰器实现进行装饰之后,每次从缓存中获取同一key对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程以及缓存中的对象;而SerializedCache每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。SerializedCache使用的序列化方式是Java原生序列化,代码比较简单,请读者参考代码学习。

CacheKey

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

private final int multiplier; //参与计算hashCode,默认是37
private int hashcode; //CacheKey对应的hashCode 默认是17
private long checksum;    //校验和
private int count;        //updateList集合的个数
// 8/21/2017 - Sonarlint flags this as needing to be marked transient.  While true if content is not serializable, this is not always true and thus should not be marked transient.
private List<Object> updateList;  //由该集合所有对象共同决定两个CacheKey是否相同

在第3章的介绍中,可以见到下面四个部分构成的CacheKey对象,也就是说这四部分都会记录到该CacheKey对象的updateList集合中:

  • MappedStatementid.
  • 指定查询结果集的范围,也就是RowBounds.offsetRowBounds.limit.
  • 查询所使用的SQL语句,也就是boundSql.getSql()方法返回的SQL语句,其中可能包含“?”占位符。
  • 用户传递给上述SQL语句的实际参数值。

在向CacheKey.updateList集合中添加对象时,使用的是CacheKey.update()方法,具体实现如下:

public void update(Object object) {
  int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); //添加数组或集合类型

  count++;
  checksum += baseHashCode;
  baseHashCode *= count;

  //重新计算count、checksum和hashcode,并将该对象添加到updateList集合中
  hashcode = multiplier * hashcode + baseHashCode;

  updateList.add(object);
}

CacheKey重写了equals()方法和hashCode()方法, 这两个方法使用上面介绍的countchecksumhashcodeupdateList比较CacheKey对象是否相同,具体实现如下:

public boolean equals(Object object) {
  if (this == object) { //是否是同一对象
    return true;
  }
  if (!(object instanceof CacheKey)) {  //是否类型相同
    return false;
  }

  final CacheKey cacheKey = (CacheKey) object;

  if (hashcode != cacheKey.hashcode) {  //比较hashCode
    return false;
  }
  if (checksum != cacheKey.checksum) {  //比较checksum
    return false;
  }
  if (count != cacheKey.count) {      //比较count
    return false;
  }

  for (int i = 0; i < updateList.size(); i++) { //比较updateList中每一项
    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;
}

小结

本章主要介绍了MyBatis基础支持层中各个模块的功能和实现原理。首先介绍了XML解析的基础知识以及解析器模块的具体实现。又介绍了MyBatisJava反射机制的封装,Type接口的基础知识,以及对复杂属性表达式在类层面和对象层面的处理。然后介绍了MyBatis如何实现数据在Java类型与JDBC类型之间的转换以及MyBatis中别名的功能。之后分析了日志模块的相关实现,介绍了其中使用的适配器模式和代理模式,分析了JDK动态代理的实现原理以及在MyBatis中的应用,又分析了资源加载模块的实现。

后面紧接着介绍了MyBatis提供的DataSource模块的实现和原理,以及其中涉及的工厂方法设计模式,深入分析了MyBatis通过的数据源实现。之后简单介绍了Transaction模块的功能。然后分析了binding模块如何将Mapper接口与映射配置信息相关联,以及其中的原理。最后介绍了MyBatis的缓存模块,介绍了其中涉及的装饰器模式,分析了Cache接口以及多个实现类的具体实现,它们是第3章介绍的一级缓存和二级缓存的基础。
希望胖友通过本篇的阅读,了 解MyBatis基础支持层提供的主要功能,有助于理解后面对MyBatis核心处理层的分析。也希望胖友在实践中可以借鉴相关模块,实现类似的需求。

正文到此结束