前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis设计思想(4)——缓存模块

MyBatis设计思想(4)——缓存模块

作者头像
张申傲
发布2020-09-03 16:31:11
5980
发布2020-09-03 16:31:11
举报
文章被收录于专栏:漫漫架构路漫漫架构路

MyBatis设计思想(4)——缓存模块

一. 缓存概述

相信大家对于缓存都不陌生,MyBatis也提供了缓存的功能,在执行查询语句时首先尝试从缓存获取,避免频繁与数据库交互,大大提升了查询效率。MyBatis有所谓的一级缓存和二级缓存,这个会在后面的核心流程中详细阐述,这里仅讨论缓存的内部实现。

首先看下MyBatis的Cache接口,定义了缓存的基本行为:

代码语言:javascript
复制
public interface Cache {

  //获取缓存的唯一id
  String getId();

  //保存元素
  void putObject(Object key, Object value);

  //获取元素
  Object getObject(Object key);

  //删除元素
  Object removeObject(Object key);

  //清空缓存
  void clear();

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

我们知道,缓存的本质其实就是一个Map,MyBatis的缓存最基础的实现PerpetualCache,也是使用了一个HashMap来保存数据:

代码语言:javascript
复制
/**
 * @author Clinton Begin
 *
 * 缓存的基础实现,使用HashMap来保存数据
 */
public class PerpetualCache implements Cache {

  private final String id;  //缓存的唯一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());
  }

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

这个缓存的实现看上去平平无奇,几乎任何人都能写得出来。那么现在问题来了,作为一个成熟的ORM框架,MyBatis势必要为缓存提供各种额外的扩展功能,比如淘汰策略、锁同步功能、定时清空功能、防击穿、打印日志等。那么,如何在缓存的基础实现上,动态扩展这些功能呢?

二. 通过继承扩展

想要对一个类进行功能上的扩展,我们第一时间就会想到继承。的确,通过继承可以很方便地在现有的类上增加额外的功能。如果我们想要为缓存增加LRU淘汰策略,只需要新建一个LruCache类,继承PerpetualCache即可。同理,想要具有打印日志功能的缓存,就需要再创建一个LoggingCache类,这种解决方案看上去可以满足需求。

但是问题是,缓存的能力是动态组合和扩展的。在实际使用中,常常会见到下面这种形式的缓存配置:

代码语言:javascript
复制
<!--开启二级缓存配置-->
<cache eviction="LRU" flushInterval="60000" blocking="true" size="512"/>

这里开启了二级缓存,设置了容量上限的512,淘汰策略是LRU,每60s清空,且缓存为空时通过阻塞式从DB中查询数据,避免大量缓存击穿。这样就要求缓存实现类动态扩展LRU、定时清空、阻塞查询的功能。这样一来,如果依然通过继承的方式实现,就需要再创建LruScheduledBlockingCache类。而且,由于所有功能是动态增加的,你事先并不知道客户端会选择哪几个功能,那么就需要提前把所有功能排列组合地实现一遍,如LruScheduledCache、ScheduledBlockingCache、LruBlockingCache… 而且,每扩展一个新的功能,就需要把所有已有的缓存再排列组合一遍,最终的结果就是类爆炸。

组合优于继承。

三. 装饰器模式

装饰器模式最大的作用,就是为已有的组件动态地扩展新的功能。

在这里插入图片描述
在这里插入图片描述
  1. IComponent:定义了所有组件和装饰器的公共行为。
  2. ConcreteComponent:具体的组件,实现IComponent,是需要被装饰的原始对象,新功能或者附加功能都是通过装饰器添加到该类的对象上的。
  3. ComponentDecorator(Optional):抽象装饰器,定义了装饰器的核心行为。
  4. ComponentDecoratorImpl:具体的装饰器,对ComponentDecorator进行功能的扩展。

MyBatis缓存模块的设计就采用了装饰器模式。最基础的缓存实现PerpetualCache就是ConcreteComponent,并且在此基础上提供了多种ComponentDecoratorImpl,对缓存的功能进行扩展。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhRZSaFu-1595151903883)(/Users/zhangshenao/Desktop/mybatis/cache_decorators.png)]

基于这种装饰器模式的设计,想为缓存进行功能的动态扩展就变得十分容易了。上面那个具有多种功能的二级缓存,就可以采用这种方式创建:

代码语言:javascript
复制
Cache cache = new ScheduledCache(new BlockingCache(new LruCache(new PerpetualCache())));

这样一来,客户端就可以任意增加自己想要的缓存功能。相较于继承,装饰器模式使得组件在运行期可以根据需要动态的添加功能,甚至对添加的新功能进行自由的组合,十分灵活且扩展性强。

四. 几个有趣的缓存装饰器

下面介绍几个缓存装饰器,个人觉得还挺有意思的:

  1. LruCache MyBatis很好地使用了JDK的LinkedHashMap,LinkedHashMap支持按照访问顺序排序,将最近被访问到的元素放在队列头部,这样就天然支持了LRU。
代码语言:javascript
复制
/**
 * Lru (least recently used) cache decorator.
 *
 * @author Clinton Begin
 *
 * LRU缓存装饰器,依赖了LinkedHashMap的实现
 */
public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;

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

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

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

  public void setSize(final int size) {
    //这里使用了LinkedHashMap保存所有Cache Key
    //LinkedHashMap支持按照访问顺序排序,会将最近被访问到的元素放在队列头部
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();    //如果缓存大小超过容量上限,则记录待淘汰的key,在下次插入元素时删除
        }
        return tooBig;
      }
    };
  }

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

    //插入元素后,删除过期的元素
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    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();
  }

  //删除过期key
  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

}
  1. BlockingCache BlockingCache的作用是,当缓存Miss时,对线程加锁,保证同一时刻只有一个线程去DB执行查询操作,这样就避免了高并发场景下,缓存失效造成的大量击穿。
代码语言:javascript
复制
/**
 * Simple blocking decorator
 *
 * Simple and inefficient version of EhCache's BlockingCache decorator.
 * It sets a lock over a cache key when the element is not found in cache.
 * This way, other threads will wait until this element is filled instead of hitting the database.
 *
 * @author Eduardo Macarron
 *
 * 缓存的阻塞装饰器: 当缓存Miss时,对线程加锁,保证同时只有一个线程去DB执行查询操作,这样就避免了高并发场景下,缓存失效造成的大量击穿。
 *
 */
public class BlockingCache implements Cache {

  private long timeout;
  private final Cache delegate;
  private final ConcurrentHashMap<Object, ReentrantLock> locks;   //使用ConcurrentHashMap,按照Cache Key粒度加锁

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

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

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

  @Override
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      //释放锁
      releaseLock(key);
    }
  }

  @Override
  public Object getObject(Object key) {
    //每次查询时,首先尝试加锁
    acquireLock(key);
    Object value = delegate.getObject(key);
    if (value != null) {
      releaseLock(key);
    }
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    // despite of its name, this method is called only to release locks
    releaseLock(key);
    return null;
  }

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

  private ReentrantLock getLockForKey(Object key) {
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
  }

  private void acquireLock(Object 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 void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

  public long getTimeout() {
    return timeout;
  }

  public void setTimeout(long timeout) {
    this.timeout = timeout;
  }
}
  1. 但是,BlockingCache是每次查询时都会进行加锁操作,尽管是根据Cache Key的细粒度锁,但是对性能还是有一定的影响,这个类的作者Eduardo Macarron也说了,这是一个简单且低效的版本。
  2. ScheduledCache ScheduledCache的作用就是增加了缓存的定时清空功能。这个清空是lazy的,即在每次get和put操作时,会校验距离上次执行clear操作的时间是否已超过clearInterval。如果超过,则执行一次clear。
代码语言:javascript
复制
/**
 * @author Clinton Begin
 *
 * 具有定时清空功能的缓存装饰器
 * lazy模式,在每次get和put操作时,会校验距离上次执行clear操作的时间是否已超过clearInterval。如果超过,则执行一次clear
 */
public class ScheduledCache implements Cache {

  private final Cache delegate;
  protected long clearInterval;
  protected long lastClear;

  public ScheduledCache(Cache delegate) {
    this.delegate = delegate;
    this.clearInterval = TimeUnit.HOURS.toMillis(1);
    this.lastClear = System.currentTimeMillis();
  }

  public void setClearInterval(long clearInterval) {
    this.clearInterval = clearInterval;
  }

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

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

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

  @Override
  public Object getObject(Object key) {
    return clearWhenStale() ? null : delegate.getObject(key);
  }

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

  @Override
  public void clear() {
    lastClear = System.currentTimeMillis();
    delegate.clear();
  }

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

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


  //校验清空时间
  private boolean clearWhenStale() {
    if (System.currentTimeMillis() - lastClear > clearInterval) {
      clear();
      return true;
    }
    return false;
  }

}

五. CacheKey的设计

既然说到了缓存,就不得不提缓存Key的设计问题。MyBatis涉及到的查询场景十分复杂,查询的操作SQL语句、SQL参数等等信息,都会影响到缓存是否命中,使用简单的String做为缓存Key是肯定不行了,那么该如何设计呢?

MyBatis定义了CacheKey类,封装了所有影响缓存命中的因素,主要包括:

  1. mappedStatment的id(Cache id)
  2. 指定查询结果集的范围(分页信息)
  3. 查询所使用的SQL语句
  4. 用户传递给SQL语句的实际参数值

CacheKey封装了这些信息,并重写了hashCode()和equals()方法:

代码语言:javascript
复制
/**
 * @author Clinton Begin
 *
 * MyBatis定义的CacheKey
 */
public class CacheKey implements Cloneable, Serializable {

  private static final long serialVersionUID = 1146682552656046210L;

  public static final CacheKey NULL_CACHE_KEY = new CacheKey() {

    @Override
    public void update(Object object) {
      throw new CacheException("Not allowed to update a null cache key instance.");
    }

    @Override
    public void updateAll(Object[] objects) {
      throw new CacheException("Not allowed to update a null cache key instance.");
    }
  };

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

  private final int multiplier;   //乘积因子

  private int hashcode;     //hashCode

  private long checksum;    //校验和

  private int count;    //影响因素数量

  // 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;  //影响因素

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

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

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

  //每次增加CacheKey的影响因素,都会重新计算一遍内部的各种校验值
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

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

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

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

  //判断两个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) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return 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;
  }

}

如果两个CacheKey的hashCode()相等,且equals()方法返回true,则认为是同一个查询操作,可以直接从缓存中获取数据。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-07-19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MyBatis设计思想(4)——缓存模块
    • 一. 缓存概述
      • 二. 通过继承扩展
        • 三. 装饰器模式
          • 四. 几个有趣的缓存装饰器
            • 五. CacheKey的设计
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档