MyBatis 缓存模块源码分析

发布于 2024-01-29 08:33:32 字数 16528 浏览 30 评论 0

优秀的 ORM 框架都应该提供缓存机制,MyBatis 也不例外,在 org.apache.ibatis.cache 包下面定义了 MyBatis 缓存的核心模块,需要注意的是这个包中只是 MyBatis 缓存的核心实现,并不涉及一级缓存和二级缓存的实现,本文同样没有涉及到一二级缓存的具体实现方式的讲解。

在阅读缓存模块源码之前,读者们应该首先弄懂装饰器模式的含义,因为缓存模块的实现是装饰器模式的一种最佳实践,只有弄懂了装饰器模式才能更好的理解缓存模块的原理。

一. 初探缓存模块源码结构

缓存模块源码结构如下:

decorators 包:该包存放的是装饰器,通过这些装饰器可以为缓存添加一下核心功能,例如:防止缓存击穿,添加缓存清空策略,日志功能、序列化功能、定时清空功能。

  • BlockingCache :防止缓存击穿的装饰器。
  • FifoCache :缓存淘汰策略装饰器(先进先出)。
  • LoggingCache :缓存的日志装饰器,用于输出缓存命中率。
  • LruCache :缓存淘汰策略装饰器(最近最少使用)
  • ScheduledCache :缓存清空计划装饰器,该装饰器目的是实现每小时清空一次缓存。
  • SerializedCache :缓存值序列化装饰器。将对象存存入缓存之前将对象序列化为字节数组存入缓存,在 get 时反序列化为对象。
  • SoftCache :软引用缓存装饰器。
  • SynchronizedCache :同步装饰器,用于保证缓存的更新操作是线程安全的。
  • TransactionalCache :二级缓存事务缓冲区。
  • WeakCache :弱引用缓存装饰器。

impl 包:该包下只有一个 PerpetualCache 类,它就是缓存模块的核心类。

一. 缓存模块的核心接口(Cache)

/**
 * MyBatis 缓存模块采用装饰器模式
 */
public interface Cache {

  /**
   * @return 获取这个缓存的 ID
   */
  String getId();

  void putObject(Object key, Object value);

  Object getObject(Object key);

  Object removeObject(Object key);

  void clear();

  int getSize();

  //获取读写锁。从 3.2.6 开始,这个方法不再被核心调用
  default ReadWriteLock getReadWriteLock() {
    return null;
  }
}

可以看到 putObject 方法的 key 不是使用的 String 而是 Object,这是因为 MyBatis 涉及动态 SQL 的原因,缓存项的 key 不能仅仅通过一个 String 来表示,所以通过 CacheKey 来封装缓存的 Key。在 CacheKey 中封装类多个影响缓存项 的因素。

构成 CacheKey 的对象:

  • mappedStatement 的 id
  • 指定查询结果集的范围(分页信息)
  • 查询所使用的 SQL 语句
  • 用户传递给 SQL 语句的实际参数值
/**
 * 缓存 key
 * MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]生成一个哈希码
 */
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;//参与 hash 计算的乘数
  private int hashcode; //当前 CacheKey 的 HashCode
  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;

  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();
  }

  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++; //updateList 中的 size +1
    checksum += baseHashCode;//计算校验和
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode; //更新 Hash 值

    updateList.add(object);
  }

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

  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    /**
     * 这三个值都相同 equals 才有可能相同,目的是快速剔除大部分不相同的对象
     */
    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;
  }
}

二. 缓存的核心实现(PerpetualCache)

Mybatis 缓存实现是基于 HashMap 实现的,所以 PerpetualCache 实现了实际上是线程不安全的。如果想要实现线程安全的缓存,就需要使用 SynchronizedCache 装饰器装饰 PerpetualCache

/**
 * MyBatis 缓存的核心实现类。类似于 IO 中的 FileInputStream(JDK 中的 IO 同样采用装饰器模式)
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {
  //每一个缓存都有一个唯一 ID
  private final String id;

  //缓存底层使用 HashMap 保存数据,所以单独的 PerpetualCache 实例是不支持多线程的,而 decorators 包中提供的装饰器可以将其包装为 Blocking
  private final Map<Object, Object> cache = new HashMap<>();

  ...

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

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

  ...
}

三. 装饰器

由于缓存模块的装饰器比较多,下面抽出几个有代表性的装饰器讲解,其他的装饰器原理可以参考博主的中文注释项目:

3.1 BlockingCache

BlockingCache 装饰器是为了防止缓存击穿,保证只有一个线程根据指定的 key 到数据库中查找对应的数据,使用 ConcurrentHashMap (安全,不重复)去存放锁的 key(按照 key 加锁的细粒度锁)。

/**
 * 装饰器,让缓存拥有阻塞的功能,目的是为了防止缓存击穿。(当在缓存中找不到元素时,它设置对缓存键的锁定,这样,其他线程将一直等待,直到该缓存键放入了缓存值)
 *
 * 需要注意的是,这个装饰器并不能保证缓存操作的线程安全
 *
 * 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
 *
 */
public class BlockingCache implements Cache {

  //超时时间
  private long timeout;
  //被装饰的对象
  private final Cache delegate;
  /**
   * 这里采用分段锁,每一个 Key 对应一个锁,当在缓存中找不到元素时,它设置对缓存键的锁定。
   * 这样,其他线程将一直等待,直到该缓存键放入了缓存值,这也是防止缓存击穿的典型方案。
   *
   * 需要注意的是,这里每一个 Key 都对应一个锁,就并不能保证对底层 Map 的更新操作(主要是 put 操作),
   * 只由一个线程执行,那这样多线程状态下对底层 HashMap 的更新操作也是线程不安全的!
   *
   * 也就是说,BlockingCache 只是为了解决缓存击穿的问题,而不是解决缓存操作的线程安全问题,
   * 线程安全问题交由 SynchronizedCache 装饰器来完成
   *
   */
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

  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);
    //调用被装饰对象的 getObject 方法获取缓存
    Object value = delegate.getObject(key);
    if (value != null) {
      /**
       * 如果 value 不为空,则释放锁。
       * 这里很多人肯定会有疑问,如果没有获取到值难道就不释放锁了吗?其实不然,当
       * MyBatis 获取缓存时没有获取到数据,则会真正执行 SQL 语句去查询数据库,查询到结果后
       * 会紧接着调用缓存的 putObject 方法,在这个方法中会进行释放锁的操作
       */
      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) {
    //如果存在 key 对应的锁则返回已经存在的锁,如果不存在则创建一个并返回
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
  }

  /**
   * 尝试获取锁
   * @param key 缓存的 key,
   */
  private void acquireLock(Object key) {
    //获取锁对象
    Lock lock = getLockForKey(key);
    if (timeout > 0) {
      //如果锁的获取拥有超时时间
      try {
        //则尝试在指定时间内获取锁,如果获取到了则返回 true,超时仍未获取到则返回 false
        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;
  }
}

3.2 SynchronizedCache

/**
 * 同步装饰器,用于保证缓存的更新操作是线程安全的
 */
public class SynchronizedCache implements Cache {

  private final Cache delegate;

  public SynchronizedCache(Cache delegate) {
    this.delegate = delegate;
  }

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

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

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

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

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

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

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

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

}

同步装饰器,用于保证缓存的更新操作是线程安全的。它对所有涉及所有对缓存的操作(读/写)都使用 synchronized 关键字进行同步。需要注意的是 SynchronizedCache 装饰器是将整个方法进行了加锁,这在使用的时候就应该在保证线程安全的情况下,尽量的将这个装饰器写在内层,这样可以减小锁的范围,增加系统吞吐量。例如在 LoggingCacheSynchronizedCache 装饰器同时使用的情况下, LogingCache 装饰器最好放在 SynchronizedCache 装饰器外层。

new LoggingCache( new SynchronizedCache( new PerpetualCache() ) )

3.3 FifoCache

该装饰器用于使缓存具有先进先出淘汰机制,当缓存中缓存的数量到达阈值时,就会触发缓存淘汰,而淘汰的目标就是最先进入缓存的对象。

/**
 * 缓存淘汰策略装饰器(先进先出)
 *
 * @author Clinton Begin
 */
public class FifoCache implements Cache {

  /**
   * 被装饰对象
   */
  private final Cache delegate;
  /**
   * 双端队列
   */
  private final Deque<Object> keyList;
  /**
   * 缓存的上限个数,触发这个值就会激活缓存淘汰策略
   */
  private int size;

  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<>();
    this.size = 1024;
  }

  ...

  @Override
  public void putObject(Object key, Object value) {
    //将缓存键放入队列中,如果队列超长,则删除队首的键,以及其对应的缓存
    cycleKeyList(key);
    delegate.putObject(key, value);
  }

  ...

  /**
   * 将缓存键放入队列中,如果队列超长,则删除队首的键,以及其对应的缓存
   * @param key
   */
  private void cycleKeyList(Object key) {
    //在双端队列队尾放入缓存键
    keyList.addLast(key);
    //如果缓存个数大于了极限值
    if (keyList.size() > size) {
      //移除双端队列对数的保存的 Key
      Object oldestKey = keyList.removeFirst();
      //通过这个 Key 删除队首元素
      delegate.removeObject(oldestKey);
    }
  }
}

3.4 LruCache

该装饰器用于使缓存具有 LRU(最近最少使用)淘汰机制,当缓存中缓存的数量到达阈值时,就会触发缓存淘汰,而淘汰的目标就是最近最少使用的对象。LruCache 中是借助 LinkedHashMap 来实现 LRU 淘汰算法的,想要看到 LruCache 的源码,就需要读者们先去研究一下 LinkedHashMap 的源码才能理解,这里面的淘汰策略是如何实现的.

/**
 * 缓存淘汰策略装饰器(最近最少使用)
 *
 * @author Clinton Begin
 */
public class LruCache implements Cache {

  private final Cache delegate;
  //使用 LinkedHashMap 维护 key 的使用顺序,个人认为这种实现方式虽然巧妙,但并不算特别优雅
  private Map<Object, Object> keyMap;
  private Object eldestKey;//最近最久没有使用的 Key

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

  ...
                        
  public void setSize(final int size) {
    //匿名内部类
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      /**
       * 重写 LinkedHashMap 中的 removeEldestEntry 方法是实现 LRU 的核心,
       * 这里 LRU 的实现机制不清楚的话可以研究一下 LinkedHashMap 源码。
       *
       * 大致思路:LinkedHashMap 继承于 HashMap 并在 HashMap 的基础上为每一个 Map.Entry 添加了一个 before 和 after 指针,从而
       * 实现链表的功能,并全局维护了一个全局的 head 和 tail 指针,分别指向链表的头和尾。当 LinkedHashMap put 元素时,会将
       * 这个 Entry 加入链表的末尾。这样链表头始终指向的是最近最久没有使用的元素,在 put 方法中会调用 removeEldestEntry 方法
       * 判断是否删除最久没有使用的元素,如果返回 true 则会删除最久没有会用过的元素的元素。
       * @param eldest
       * @return
       */
      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          //保存最近最久没有使用的 key
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    //增加元素后,进行 key 的后处理。判断是否触发缓存数极限,如果触发了则清除最老的 key 对应的缓存
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    //调用 LinkedHashMap.get() 方法是为了让这次使用的 key 移动到链表末尾
    keyMap.get(key); // touch
    return delegate.getObject(key);
  }

  private void cycleKeyList(Object key) {
    //将 key 放入 keyMap,如果缓存数大于了极限值,则会删除最老 Key,并将这个 key 赋给 eldestKey 变量
    keyMap.put(key, key);
    if (eldestKey != null) {
      //eldestKey 不为空,说明在调用 keyMap.put() 中触发了删除最老元素的机制,此时需要将真实缓存中对应的 key-value 删除
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }
}
/**
   * 将连接对象归还给连接池(实际上是将连接从 active 队列中移到 idle 队列中)
   *
   * @param conn
   * @throws SQLException
   */
  protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {
      //将当前连接从 active 队列中移除
      state.activeConnections.remove(conn);
      if (conn.isValid()) {
        //连接是有效的
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
          //空闲的数量小于最大空闲值 且 连接对象的 typeCode 与数据源期望的 TypeCode 相同
          //记录当前连接使用的时间
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            //如果连接对象的事务是非自动提交的,则回滚事务
            conn.getRealConnection().rollback();
          }
          //重新封装一个新的代理连接对象
          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
          //将新建的代理连接对象放入空闲队列
          state.idleConnections.add(newConn);
          //设置创建的时间戳
          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
          //设置最后使用的时间戳
          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
          //老的代理对象作废
          conn.invalidate();
          if (log.isDebugEnabled()) {
            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
          }
          //换新在 state 锁上的等待的线程
          state.notifyAll();
        } else {
          //空闲的数量大于等于最大空闲值 或者 连接对象的 typeCode 与数据源期望的 TypeCode 不相同
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          //关闭这个连接
          conn.getRealConnection().close();
          if (log.isDebugEnabled()) {
            log.debug("Closed connection " + conn.getRealHashCode() + ".");
          }
          //将代理连接作废
          conn.invalidate();
        }
      } else {
        if (log.isDebugEnabled()) {
          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
        }
        state.badConnectionCount++;
      }
    }
  }

3.5 其他装饰器

其他装饰器代码注释请移步至GitHubGitee

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

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

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
615 人气
更多

推荐作者

qq_E2Iff7

文章 0 评论 0

Archangel

文章 0 评论 0

freedog

文章 0 评论 0

Hunk

文章 0 评论 0

18819270189

文章 0 评论 0

wenkai

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文