Mybatis源码学习第三课---基础支持层缓存模块分析

     MyBatis 作为一个强大的持久层框架,缓存是其必不可少的功能之一。 MyBatis 中的缓存是两层结构的,分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是 Cache 接 口的实现。

     缓存模式主要是设计模式是装饰器模式。

在 MyBatis 的缓存模块中,使用了装饰器模式的变体,其中将 Decorator 接口和 Component接口合并为一个 Component 接口,得到的类问结构下图所示。
   

        使用装饰器模式的有两个明显的优点:

        1、相较于继承来说,装饰器模式的灵活性更强,可扩展性也强。正如前面所说,继承方 式会导致大量子类的情况。而装饰者模式可以将复杂的功能切分成一个个独立的装饰 器,通过多个独立装饰器的动态组合,创建不同功能的组件,从而满足多种不同需求。

       2、当有新功能需要添加时,只需要添加新的装饰器实现类,然后通过组合方式添加这个 新装饰器即可,无须修改己有类的代码,符合“开放一封闭”原则。

     但是,随着添加的新需求越来越多,可能会创建出嵌套多层装饰器的对象,这增加了系统 的复杂性, 也增加了理解的难度和定位错误的难度。

一、Cache 接口及其实现

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

public interface Cache {

  //该缓存对象的id
  String getId();

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

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

  //删除 key 对应的缓存项
  Object removeObject(Object key);
  //清空缓存
  void clear();
  //缓存项的个数,该方法不会被 MyBatis 核心代码使用,所以可提供空实现
  int getSize();
  //获取读写锁,该方法不会被 MyBatis 核心代码使用,所以可提供空实现
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

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

1.1 Perpetual Cache 

     Perpetual Cache 在缓存模块中扮演着 ConcreteComponent 的角色,其实现比较简单,底层使 用 HashMap 记录缓存项,也是通过该 HashMap 对象的方法实现的 Cache 接口中定义的相应方 法。 PerpetualCache 的具体实现如下:

public class PerpetualCache implements Cache {
  //Cache的唯一标识
  private final String id;

  //使用MAP记录缓存
  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();}
// ... 重写 了 equals ()方法和 hashCode ()方法,两者都只关心 id 字段,并不关心 cache 字段(略)

}

        下面来介绍 cache.decorators 包下提供的装饰器,它们都直接实现了 Cache 接口,扮演着 ConcreteDecorator 的角色。这些装饰器会在 PerpetualCache 的基础上提供一些额外的功能,通 过多个组合后满足一个特定的需求,后面介绍二级缓存时,会见到这些装饰器是如何完成动态 组合的。

1.2 BlockingCache 

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

  

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

private long timeout;//阻塞的超时时长
  private final Cache delegate;//被装饰的底层对象,一般是PerpetualCache
  //每个 key 都有对应的 ReentrantLock 对象
  private final ConcurrentHashMap<Object, ReentrantLock> locks;//锁对象集,粒度到key值

  public BlockingCache(Cache delegate) {
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<>();
  }

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

                  

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

 /**
   * 细粒度锁
   * 在get的时候先去获得key的锁
   * @param key The key
   * @return
   */
  @Override
  public Object getObject(Object key) {
    //尝试去获取锁
    acquireLock(key);
    Object value = delegate.getObject(key);
    //缓存有 key 对应的缓存项,择放锁,否则继续持有锁
    if (value != null) {
      //释放锁
      releaseLock(key);
    }
    return value;
  }

  acquireLock(key)会去尝试获取当前key的锁,如果当前key没有对象锁则为其创建新的ReentrantLock对象,在对其加锁。如果有改key的对象锁存在则使用该对象,对其加锁。如果加锁失败,阻塞一段时间。其代码如下:

      

//acquireLock()方法中会尝试获取指定 key 对应的锁。
  // 如果该 key 没有对应的锁对象则为其创建新的 ReentrantLock 对象,再加锁;如果获取锁失败, 则阻塞一段时间。
  private void acquireLock(Object key) {
    //获取ReentrantLock 对象
    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) {
    //把新锁添加到locks集合中,如果添加成功使用新锁,如果添加失败则使用locks集合中的锁
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
  }

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

                                                      

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

 @Override
  public void putObject(Object key, Object value) {
    try {
      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 是先进先出版本的装饰器,当向缓存添加数据时,如果缓存数据个数已经达到上线,则清除最先进来的缓存数据。

     FifoCache中属性如下所示:

  private final Cache delegate;//底层被装饰的cache对象
  //用于记录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;
  }

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

 @Override
  public void putObject(Object key, Object value) {
    cycleKeyList(key);//把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<Obj ect, Object>类型对象,它是一个有序的 HashMap,用于记录 key 最近的使用情况
  private Map<Object, Object> keyMap;
  private Object eldestKey;//记录最少被使用的缓存项的key

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

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

//重新设置缓存大小时,会重置 keyMap 字段
  public void setSize(final int size) {
    //LinkedHashMap的第三个参数为true,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;
      }
    };
  }

        为了让读者更好地理解  LinkedHashMap,下图 展示了其原理,图中的虚线形成了一个队 列,当 LinkedHashMap.get()方法访问 K1时,会修改这条虚线队列将 K1项移动到队列尾部, LruCache 就是通过 LinkedHashMap 的这种特性来确定最久未使用的缓存项。

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

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); //修改key中在记录中的顺序
    return delegate.getObject(key);
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);//加入缓存
    cycleKeyList(key);//把key加入记录中并会清除缓存
  }
  private void cycleKeyList(Object key) {
    //
    keyMap.put(key, key);
    if (eldestKey != null) {//eldestKey 不为空,表示已经达到缓存上
      delegate.removeObject(eldestKey);//清除最少使用的缓存
      eldestKey = null;
    }
  }

1.4 SoftCache&WeakCache 

    Java 提供的 4 种引 用类型,它们分别是强引用 (Strong Reference)、软引用 ( soft Reference)、弱引用(Weak Reference) 和幽灵引用(Phantom Reference)。详情可以自行了解。

    SoftCache的实现。SoftCache中各个字段的 含义如下:

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

  public SoftCache(Cache delegate) {
    this.delegate = delegate;
    this.numberOfHardLinks = 256;
    this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
    this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
  }

   SoftCache 中缓存项的 value 是 SoftEntry对象, SoftEntry 继承了 SoftReference<Object>, 其中指向 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;///key是强引用
    }
  }

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

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

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

@Override
  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();
      if (result == null) {//已经被 GC 回收
        delegate.removeObject(key);//从缓存中清除对应的缓存项 
      } else {
        ///缓存项的 value 添加到 hardLinksToAvoidGarbageCollection 集合中保存
        // See #586 (and #335) modifications need more than a read lock
        synchronized (hardLinksToAvoidGarbageCollection) {
          hardLinksToAvoidGarbageCollection.addFirst(result);
          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
            hardLinksToAvoidGarbageCollection.removeLast();
          }
        }
      }
    }
    return result;
  }

1.5 ScheduledCache&LoggingCache&Synchronized&CacheSerializedCache 

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

             LoggingCache 在 Cache 的基础上提供了日志功能,它通过 hit 宇段和 request 字段记录了 Cache 的命中次数和访问次数。在 LoggingCache.getO均ect()方法中会统计命中次数和访问次数 这两个指标,井按照指定的日志输出方式输出命中率。 LoggingCache 代码比较简单,请读者参 考代码学习。

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

        Serialized Cache 提供了将 value 对象序列化的功能。 S巳rializedCache 在添加缓存项时,会将 value 对应的 Java 对象进行序列化,井将序列化后的 byte[]数组作为 value 存入缓存 。 Serialized Cache 在获取缓存项时,会将缓存项中的 byte[]数组反序列化成 Java 对象。使用前面 介绍的 Cache 装饰器实现进行装饰之后,每次从缓存中获取同- key 对应的对象时,得到的都 是同一对象,任意一个线程修改该对象都会影 响到其他线程以及缓存中的对象:而 SerializedCache 每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。 

      二、CacheKey

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

    CacheKey中的核心字段:

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

  private final int multiplier;//参与hash计算的乘数 默认是37
  private int hashcode;//CacheKey的hash值,在update函数中实时运算出来的
  private long checksum;//校验和
  private int count;//updateList集合的个数
  private List<Object> updateList;//由该集合中的所有对象共同决定两个 CacheKey 是否相同

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }

     CacheKey 对象由四个部分构成,也就是说这四部分都 会记录到该 CacheKey 对象的 updateList 集合中:

        1、MappedStatement 的 id。

        2、指定查询结果集的范围,也就是 RowBounds.offset 和 RowBounds.limit。

       3、查询所使用的 SQL 语句,也就是 boundSql.getSql()方法返回的 SQL 语句,其中可能包 含“?”占位符。

       4、用户传递给上述 SQL 语句的实际参数值。

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

 public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }
public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }
  public void update(Object object) {
    //获取object的hash值
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    //更新count、checksum以及hashcode的值
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    //将对象添加到updateList中
    updateList.add(object);
  }

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

@Override
  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;
    }
    //顺序比较updateList中元素的hash值是否一致
    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;
  }

三 cache装饰者类的使用

CacheBuilder.setStandardDecorators()方法会根据 CacheBuilder 中各个字段的值,为 cache 对 象添加对应的装饰器,具体实现如下:

  

 //缓存模块装饰器的使用
  private Cache setStandardDecorators(Cache cache) {
    try {
      //获取基本的cache对象
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      //缓存的容量大小
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      //检测是否指定了 clearinterval 字段 (定期清除)
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);//添加定期清除的装饰器
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      //如果是只读缓存
      if (readWrite) {
        //是否只读,对应添加 SerializedCache 装饰器
          cache = new SerializedCache(cache);
      }
      //默认添加 LoggingCache 和 SynchronizedCache 两个装饰器
      cache = new LoggingCache(cache);
      cache = new SynchronizedCache(cache);
      if (blocking) {//阻塞装饰器
        cache = new BlockingCache(cache);
      }
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值