img

mybatis缓存

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

MyBatis缓存的实现是基于Map的,从缓存里面读写数据是缓存模块的核心基础功能

除核心功能之外,有很多额外的附加功能,如:防止缓存击穿,添加缓存情况策略(fifo、LRU),序列化功能,日志能力和定时清空能力等

附加功能可以以任意的组合附加到核心基础功能之上

装饰者模式

mybatis 缓存模块使用了装饰器模式

Mybatis缓存的核心模块就是在缓存中读写数据,但是除了在缓存中读写数据wait,还有其他的附加功能,这些附加功能可以任意的加在缓存这个核心功能上。

加载附加功能可以有很多种方法,动态代理或者继承都可以实现,但是附加功能存在多种组合,用这两种方法,会导致生成大量的子类,所以mybatis选择使用装饰器模式.(灵活性、扩展性)。

类结构

Mybatis 数据源的实现主要是在 cache包:

img

Cache 接口

一级缓存和二级缓存都是通过cache接口来实现的

Cache 接口是缓存模块的核心接口,定义了缓存的基本操作

/**
 * 缓存核心接口
 */
public interface Cache {

  /**
   * 获取缓存ID
   */
  String getId();

  /**
   * 设置缓存
   */
  void putObject(Object key, Object value);

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

  /**
   *
   * 删除缓存
   */
  Object removeObject(Object key);

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

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

  /**
   * 获取读写锁
   * @return A ReadWriteLock
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

缓存实现类PerpetualCache

在缓存模块使用了装饰器模式PerpetualCache在其中扮演ConcreteComponent 角色,使用 HashMap来实现 cache 的相关操作

/**
 * 缓存的核心实现类
 * 在装饰者模式中扮演了 ConcreteComponent(具体组件) 角色
 * 使用HashMao实现了缓存
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {
  /**
   * 缓存的ID
   */
  private final String id;
  /**
   * 缓存的主体对象 hashMap
   */
  private Map<Object, Object> cache = new HashMap<>();

  /**
   * 构造方法
   * @param id
   */
  public PerpetualCache(String id) {
    this.id = id;
  }

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

  /**
   * 获取缓存的大小
   * @return
   */
  @Override
  public int getSize() {
    return cache.size();
  }

  /**
   * 设置缓存
   * @param key 缓存key
   * @param value 缓存的Value
   */
  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  /**
   * 获取缓存
   * @param key The key
   * @return
   */
  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  /**
   * 删除缓存
   * @param key The key
   * @return
   */
  @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();
  }
}

缓存项CacheKey

为什么需要一个复杂的对象表示缓存项的key?通常来说表示一个对象的key可以用一个String对象,为什么不可以吗?

在cache中唯一确定一个缓存项需要使用缓存项的key,Mybatis中因为涉及到动态SQL等多方面因素,其缓存项的key不等仅仅通过一个String表示,所以MyBatis 提供了CacheKey类来表示缓存项的key,在一个CacheKey对象中可以封装多个影响缓存项的因素。

怎么样的查询条件算和上一次查询是一样的查询,从而返回同样的结果回去?这个问题,得从CacheKey说起。
我们先看一下CacheKey的数据结构:

/**
 * 缓存项cacheKey
 *
 * @author Clinton Begin
 */
public class CacheKey implements Cloneable, Serializable {

  private static final long serialVersionUID = 1146682552656046210L;
  //定义为Null的缓存key
  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
  //默认的乘数
  private static final int DEFAULT_MULTIPLYER = 37;
  //默认的hashCode
  private static final int DEFAULT_HASHCODE = 17;
  //定义乘数变量
  private final int multiplier;
  //hashCode
  private int hashcode;
  //校验码
  private long checksum;
  //统计
  private int count;
  //更新列表
  private List<Object> updateList;

  /**
   * 构造方法
   */
  public CacheKey() {
    //设置hashCode
    this.hashcode = DEFAULT_HASHCODE;
    //设置乘数
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    //更新列表 为空的list
    this.updateList = new ArrayList<>();
  }

  /**
   * 构造方法
   *
   * @param objects 需要缓存的数据
   */
  public CacheKey(Object[] objects) {
    //调用空的构造方法初始化缓存key
    this();
    //更新所有数据
    updateAll(objects);
  }
}

equals方法

其中最重要的是第41行的updateList这个属性,为什么这么说,因为HashMap的Key是CacheKey,而HashMap的get方法是先判断hashCode,在hashCode冲突的情况下再进行equals判断,因此最终无论如何都会进行一次equals的判断。

/**
   * 生成的equals方法
   * <p>
   * 如果是CacheKey
   * 先比较CacheKey的hashCode
   * 一样在比较 校验码
   * 还一样比较更新次数
   * 还是一样比较更新的每个值的hashCode
   * 保证每一个比较的一致性
   *
   * @param object
   * @return
   */
  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;
    //比较cachekey的hashCode
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    //比较校验码
    if (checksum != cacheKey.checksum) {
      return false;
    }
    //比较更新次数
    if (count != cacheKey.count) {
      return false;
    }
    //比较每个值的hashCode
    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;
  }

进行equals的时候经过了几次比较

  • 对象不为空
  • 对象是CacheKey类型
  • cacheKey的hashcode一致
  • cacheKey的校验码checksum一致
  • cacheKey的更新次数count一致
  • updateList列表中每一个数据的hashCode一致

经过这样负责的比较,保证每一个cacheKey是完全一致才会计算equals一致。

更新方法update

如何生产hashcode

17是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,
那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。
而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。
而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,
并使用常数 31, 33, 37, 39 和 41 作为乘子(cachekey使用37),每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),
那么这几个数就被作为生成hashCode值得备选乘数了

/**
 * 更新数据
 *
 * @param object
 */
public void update(Object object) {
  //获取hashCode 为null 则hashCode是1 否则是计算出来的hash值
  int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
  //统计更新次数
  count++;
  //校验码更新
  checksum += baseHashCode;
  //计算对象初始的hashCode 防止hash冲突
  baseHashCode *= count;
  /**
   * hashCode更新方法
   * newHashCode = oldHashCode*乘数(31,33,37,39,41)中选择一个+ObjectHashCode
   * 每次迭代乘以 乘数
   */
  hashcode = multiplier * hashcode + baseHashCode;
  //将缓存对象添加到更新列表
  updateList.add(object);
}

缓存增强

PerpetualCache是基类,其它实现的Cache的类都是对基类的扩
展,也就是装饰来包裹真实的对象。扩展了类的功能,也可以说是附加了一些方法。使得具有很好的灵活性

img

用于装饰PerpetualCache的标准装饰器共有8个(全部在 org.apache.ibatis.cache.decorators包中):

  • BlockingCache:是阻塞版本的缓存装饰器,它保证只有一个线程到数据库中查找指定key对应的数据。
  • FifoCache:先进先出算法,缓存回收策略
  • LoggingCache:输出缓存命中的日志信息
  • LruCache:最近最少使用算法,缓存回收策略
  • ScheduledCache:调度缓存,负责定时清空缓存
  • SerializedCache:缓存序列化和反序列化存储
  • SoftCache:基于软引用实现的缓存管理策略
  • SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问
  • WeakCache:基于弱引用实现的缓存管理策略
  • TransactionalCache:事务性的缓存

一级缓存和二级缓存

  • 一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在SqlSession(DefaultSqlSession)中,所以一级缓存的生命周期与SqlSession是相同的。
  • 二级缓存,又叫自定义缓存,实现了Cache接口的类都可以作为二级缓存,所以可配置如encache等的第三方缓存。二级缓存以namespace名称空间为其唯一标识,被保存在Configuration核心配置对象中。

注意

二级缓存对象的默认类型为PerpetualCache,如果配置的缓存是默认类型,则mybatis
会根据配置自动追加一系列装饰器。

Cache对象之间的引用顺序

SynchronizedCache–>LoggingCache–>SerializedCache–>ScheduledCache–>LruCache–>PerpetualCache