关于后端:第07篇Mybatis缓存装饰器

7次阅读

共计 14109 个字符,预计需要花费 36 分钟才能阅读完成。

MyBatis 对缓存的设计是十分奇妙的。花色很多, 但却不是真的花色。因为 Mybatis 只是对 Map数据结构的封装, 然而却实现了很多挺好用的能力。
如果单单从设计模式上的角度来, 其实就是典型的装璜器模式, 装璜器模式其实并不难, 所以咱们不讲设计模式, 本篇文章咱们来看看Mybatils 缓存设计奇妙的点。

官网文档

上面通过简略的代码 review 来剖析下这 11 个缓存类设计的奇妙点。(因为是对博客重构, 历史图片就没有补充, 图上只有 10 个)


一、模式分析

从目录就很清晰看出, 外围就是impl 包上面只有一个, 其余都是装璜器模式,在
decorators 包下

:::tip

其实下面就是Mybatis 对于 Cache 的外围实现, 其实看到这里还没有很多知识点. 那么咱们从中能学到什么呢? 如果真要找一条学习的点, 那么就是:

设计要面向接口设计,而不是具体实现。这样当咱们要重写 Cache,比如说咱们不想底层用 HashMap 来实现了, 其实咱们只有实现一下 Cache 接口,而后替换掉 PerpetualCache 就能够了。对于使用者其实并不感知。

:::

1.1 Cache

接口设计没有什么好讲的,提供获取和增加办法,跟 Map 接口一样。本篇咱们要一起 Review 的类都会实现该接口的。

(这句话几乎就是废话, 大佬勿喷, 就是简略揭示。意思就是其实代码不难)

public interface Cache {String getId();
  
  void putObject(Object key, Object value);
  
  Object getObject(Object key);

  Object removeObject(Object key);

  void clear();

  int getSize();
  
  ReadWriteLock getReadWriteLock();}

1.2 PerpetualCache

这个类就是 Mybatis 缓存最底层的设计, 看一下就晓得其实是对 Map 的封装。
其实咱们只有晓得他是简略的 HashMap 的封装就能够了。因为代码实战是太简略了, 没啥剖析的。

public class PerpetualCache implements Cache {
  // 惟一标识
  private final String id;
  // 就是一个 HashMap 构造
  private Map<Object, Object> cache = new HashMap<Object, Object>();

  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 ReadWriteLock getReadWriteLock() {return null;}

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

}

二、开始重头戏

从这里咱们次要一起看下, 代码设计的奇妙之处, 一个一个钻研下, 以下这 10 个类。看 Mybatis 是如何奇妙设计的。

2.1 BlockingCache

BlockingCache 是一个简略和低效的 Cache 的装璜器, 咱们次要看几个重要办法。

public class BlockingCache implements Cache {

  private long timeout;
  // 实现 Cache 接口的缓存对象
  private final Cache delegate;
  // 对每个 key 生成一个锁对象
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

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

  @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 {
      // 开释锁。为什么不加锁? 所以 get 和 put 是组合应用的,当 get 加锁, 如果没有就查询数据库而后 put 开释锁,而后其余线程就能够间接用缓存数据了。releaseLock(key);
    }
  }

  @Override
  public Object getObject(Object key) {
    //1. 当要获取一个 key, 首先对 key 进行加锁操作, 如果没有锁就加一个锁, 有锁就间接锁
    acquireLock(key);
    Object value = delegate.getObject(key);
    if (value != null) {
      //2. 如果缓存命中, 就间接解锁
      releaseLock(key);
    }
    //3. 当 value=null, 就是说没有命中缓存, 那么这个 key 就会被锁住, 其余线程进来都要期待
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    // 移除 key 的时候, 顺便分明缓存 key 的锁对象
    releaseLock(key);
    return null;
  }

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

  @Override
  public ReadWriteLock getReadWriteLock() {return null;}
  
  private ReentrantLock getLockForKey(Object key) {ReentrantLock lock = new ReentrantLock();
    ReentrantLock previous = locks.putIfAbsent(key, lock);
    // 如果 key 对应的锁存在就返回, 没有就创立一个新的
    return previous == null ? lock : previous;
  }
  
  private void acquireLock(Object key) {Lock lock = getLockForKey(key);
    //1. 如果设置超时工夫, 就能够期待 timeout 工夫(如果超时了报错)
    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 {//2. 如果没有设置, 间接就加锁(如果这个锁曾经被人用了, 那么就始终阻塞这里。期待上一个开释锁)
      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;}  
}

倡议 看代码正文

办法 解释
acquireLock 加锁操作
getObject 进来加锁, 如果缓存存在就开释锁, 不存在就不开释锁。
putObject 增加元素并开释锁
removeObject 移除 key 的时候, 顺便分明缓存 key 的锁对象
getLockForKey 如果 key 对应的锁存在就返回, 没有就创立一个新的

思考

  1. 这个因为每次 key 申请都会加 lock 真的会很慢吗? 咱们举两种场景。

留神这个加 lock 并不是对 get 办法加 lock, 而是对每个要 get 的 key 来加 lock。

场景一: 试想一种场景, 当有 10 个线程同时从数据库查问一个 key 为 123 的数据时候,当第一个线程来首先从 cache 中读取时候,这个时候其余九个线程是会阻塞的,因为这个 key 曾经被加 lock 了。当第一个线程 get 这个 key 实现时候,其余线程能力持续走。这种场景来说是不好的,

场景二: 然而当第一个线程来发现 cache 外面没有数据这个时候其余线程会阻塞,而第一个线程会从 db 中查问,而后在 put 到 cache 外面。这样其余 9 个线程就不须要在去查问 db 了, 就缩小了 9 次 db 查问。

2.2 FifoCache

FIFO(First Input First Output), 简略说就是指先进先出

如何实现先进先出呢? 其实非常简单, 当 put 时候, 先判断是否须要执行淘汰策略, 如果要执行淘汰, 就 移除先进来的。间接通过 Deque API 来实现先进先出。

  private final Cache delegate;
  private final Deque<Object> keyList;
  private int size;

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

@Override
  public void putObject(Object key, Object value) {
      //1. put 时候就判断是否须要淘汰
    cycleKeyList(key);
    delegate.putObject(key, value);
  }
  private void cycleKeyList(Object key) {keyList.addLast(key);
    //1. size 默认如果大于 1024 就开始淘汰
    if (keyList.size() > size) {
      //2. 利用 Deque 队列移除第一个。Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }

2.3 LoggingCache

从名字上看就是跟日志无关,LoggingCache 会在 debug级别下把缓存命中率给统计进去, 而后通过日志零碎打印进去。

public Object getObject(Object key) {
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {hits++;}
    //1. 打印缓存命中率
    if (log.isDebugEnabled()) {log.debug("Cache Hit Ratio [" + getId() + "]:" + getHitRatio());
    }
    return value;
  }

除此之外没有什么其余性能。咱们次要看下他是如何统计缓存命中率的。其实很简略。

public class LoggingCache implements Cache {

  private final Log log;
  private final Cache delegate;
  //1. 总申请次数
  protected int requests = 0;
  //2. 命中次数
  protected int hits = 0;
 
  ...
}  

在 get 申请时候无论是否命中, 都自增总申请次数(request ), 当 get 命中时候自增命中次数(hits )

public Object getObject(Object key) {//1. 无论是否命中, 都自增总申请次数( `request`)
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {//2. get 命中时候自增命中次数( `hits`)
      hits++;
    }
    if (log.isDebugEnabled()) {log.debug("Cache Hit Ratio [" + getId() + "]:" + getHitRatio());
    }
    return value;
  }

而后咱们看命中率怎么算 getHitRatio()

命中率 = 命中次数 / 总申请次数

 private double getHitRatio() {return (double) hits / (double) requests;
  }

2.4 LruCache

LRU 是 Least Recently Used 的缩写,即最近起码应用。

首先咱们看如何实现 LRU 策略。
它其实就是利用 LinkedHashMap来实现 LRU 策略, JDK 提供的 LinkedHashMap人造就反对 LRU 策略。
LinkedHashMap 有一个特点如果开启 LRU 策略后, 每次获取到数据后, 都会把数据放到最初一个节点,这样第一个节点必定是最近起码用的元素。

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        //1. 判断是否开始 LRU 策略
        if (accessOrder)
            //2. 开启就往后面放
            afterNodeAccess(e);
        return e.value;
    }

结构中先申明 LRU 淘汰策略, 当 size()大于结构中申明的 1024 就能够在每次
putObject 时候将要淘汰的移除掉。这点十分的奇妙, 不晓得你学习到了没 ?

2.5 ScheduledCache

定时删除, 设计奇妙, 能够借鉴。

public class ScheduledCache implements Cache {

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

  public ScheduledCache(Cache delegate) {
    this.delegate = delegate;
    //1. 指定多久清理一次缓存
    this.clearInterval = 60 * 60 * 1000; // 1 hour
    //2. 设置初始值
    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() {
    //1. 记录最近删除一次工夫戳
    lastClear = System.currentTimeMillis();
    //2. 清理掉缓存信息
    delegate.clear();}

  @Override
  public ReadWriteLock getReadWriteLock() {return null;}

  @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;
  }

}

外围代码

  1. 结构中指定多久清理一次缓存(1 小时)
  2. 设置初始值
  3. clearWhenStale() 外围办法
  4. 而后在每个办法中调用一次这段代码, 判断是否须要清理。
private boolean clearWhenStale() {
    //1. 以后工夫 - 最初清理工夫, 如果大于定时删除工夫, 阐明要执行清理了。if (System.currentTimeMillis() - lastClear > clearInterval) {clear();
      return true;
    }
    return false;
  }

2.6 SerializedCache

从名字上看就是反对序列化的缓存, 那么咱们就要问了,为啥要反对序列化?

为啥要反对序列化?

因为如果多个用户同时共享一个数据对象时,同时都援用这一个数据对象。如果有用户批改了这个数据对象,那么其余用户拿到的就是曾经批改过的对象,这样就是呈现了线程不平安。

如何解决这种问题

  1. 加锁当一个线程在操作时候, 其余线程不容许操作
  2. 新生成一个对象, 这样多个线程获取到的数据就不是一个对象了。

只看一下外围代码

  1. putObject 将对象序列化成byte[]
  2. getObjectbyte[] 反序列化成对象
public void putObject(Object key, Object object) {if (object == null || object instanceof Serializable) {//1. 将对象序列化成 byte[]
      delegate.putObject(key, serialize((Serializable) object));
    } else {throw new CacheException("SharedCache failed to make a copy of a non-serializable object:" + object);
    }
  }
private byte[] serialize(Serializable value) {
    try {ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(bos);
      oos.writeObject(value);
      oos.flush();
      oos.close();
      return bos.toByteArray();} catch (Exception e) {throw new CacheException("Error serializing object.  Cause:" + e, e);
    }
  }

 public Object getObject(Object key) {Object object = delegate.getObject(key);
    //1. 获取时候将 byte[]反序列化成对象
    return object == null ? null : deserialize((byte[]) object);
  }
  private Serializable deserialize(byte[] value) {
    Serializable result;
    try {ByteArrayInputStream bis = new ByteArrayInputStream(value);
      ObjectInputStream ois = new CustomObjectInputStream(bis);
      result = (Serializable) ois.readObject();
      ois.close();} catch (Exception e) {throw new CacheException("Error deserializing object.  Cause:" + e, e);
    }
    return result;
  }

这种就相似于深拷贝, 因为简略的浅拷贝会呈现线程平安问题, 而这种方法, 因为字节在被反序列化时,会在创立一个新的对象,这个新的对象的数据和原来对象的数据截然不同。所以说跟深拷贝一样。

Java 开发之深浅拷贝

2.7 SoftCache

从名字上看,Soft 其实就是软援用。软援用就是如果内存够,GC 就不会清理内存, 只有当内存不够用了会呈现 OOM 时候, 才开始执行 GC 清理。

如果要看明确这个源码首先要先理解一点垃圾回收, 垃圾回收的前提是还有没有别的中央在援用这个对象了。如果没有别的中央在援用就能够回收了。
本类中为了阻止被回收所以申明了一个变量 hardLinksToAvoidGarbageCollection
也指定了一个将要被回收的垃圾队列queueOfGarbageCollectedEntries

这个类的次要内容是当缓存 value 曾经被垃圾回收了,就主动把 key 也清理。

Mybatis 在理论中并没有应用这个类。

public class SoftCache implements Cache {
  private final Deque<Object> hardLinksToAvoidGarbageCollection;
  private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
  private final Cache delegate;
  private int numberOfHardLinks;

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

先看下变量申明

hard Links To Avoid Garbage Collection
硬连贯, 防止垃圾收集
queue Of Garbage Collected Entries
垃圾要收集的队列
number Of Hard Links
硬连贯数量

@Override
  public void putObject(Object key, Object value) {
    //1. 革除曾经被垃圾回收的 key
    removeGarbageCollectedItems();
    //2. 留神看 SoftEntry(), 申明一个 SoftEnty 对象, 指定垃圾回收后要进入的队列
    //3. 当 SoftEntry 中数据要被清理, 会增加到类中申明的垃圾要收集的队列中
    delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
  }

  @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) {
        //1. 如果数据曾经没有了, 就清理这个 key
        delegate.removeObject(key);
      } else {// See #586 (and #335) modifications need more than a read lock 
        synchronized (hardLinksToAvoidGarbageCollection) {
          //2. 如果 key 存在, 读取时候加一个锁操作, 并将缓存值增加到硬连贯汇合中, 防止垃圾回收
          hardLinksToAvoidGarbageCollection.addFirst(result);
          //3. 结构中指定硬链接最大 256, 所以如果曾经有 256 个 key 的时候回开始删除最先增加的 key
          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {hardLinksToAvoidGarbageCollection.removeLast();
          }
        }
      }
    }
    return result;
  }

  @Override
  public void clear() {
    // 执行三清
    synchronized (hardLinksToAvoidGarbageCollection) {
      //1. 革除硬链接队列
      hardLinksToAvoidGarbageCollection.clear();}
    //2. 革除垃圾队列
    removeGarbageCollectedItems();
    //3. 革除缓存
    delegate.clear();}

  private void removeGarbageCollectedItems() {
    SoftEntry sv;
    // 革除 value 曾经 gc 筹备回收了, 就就将 key 也清理掉
    while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {delegate.removeObject(sv.key);
    }
  }

2.8 SynchronizedCache

从名字看就是同步的缓存, 从代码看即所有的办法都被 synchronized 润饰。

2.9 TransactionalCache

从名字上看就应该能隐隐感觉到跟事务无关, 然而这个事务呢又不是数据库的那个事务。只是相似而已是, 即通过 java 代码来实现了一个暂存区域, 如果事务胜利就增加缓存,事务失败就回滚掉或者说就把暂存区的信息删除, 不进入真正的缓存外面。这个类是比拟重要的一个类, 因为所谓的二级缓存就是指这个类。既然说了🎧缓存就顺便提一下一级缓存。然而说一级缓存就设计到 Mybatis架构外面一个 Executor 执行器

所有的查问都先从一级缓存中查问

看到这里不禁己提一个面试题, 面试官会问你晓得 Mybatis 的一级缓存吗?
个别都会说 Mybatis 的一级缓存就是 SqlSession 自带的缓存, 这么说也对就是太抽象了,因为 SqlSession 其实就是生成 Executor 而一级缓存就是外面 query 办法中的 localCache。这个时候咱们就要看下了 localCache 到底是什么?
看一下结构, 忽然恍然大悟。原来本篇文章讲的根本就是一级缓存的实现呀。

说到这里感觉有点跑题了,咱们不是要看 TransactionalCache 的实现吗?

clearOnCommit 为 false 就是这个事务曾经实现了, 能够从缓存中读取数据了。

clearOnCommittrue , 这个事务正在进行中呢? 来的查问都给你返回 null , 等到 commit 提交时候在查问就能够从缓存中取数据了。

public class TransactionalCache implements Cache {private static final Log log = LogFactory.getLog(TransactionalCache.class);
    // 真正的缓存
  private final Cache delegate;
  // 是否清理曾经提交的实物
  private boolean clearOnCommit;
  // 能够了解为暂存区
  private final Map<Object, Object> entriesToAddOnCommit;
  // 缓存中没有的 key
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<Object, Object>();
    this.entriesMissedInCache = new HashSet<Object>();}

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

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

  @Override
  public Object getObject(Object key) {
    // 先从缓存中拿数据
    Object object = delegate.getObject(key);
    if (object == null) {
      // 如果没有增加到 set 汇合中
      entriesMissedInCache.add(key);
    }
    // 返回数据库的数据。if (clearOnCommit) {return null;} else {return object;}
  }

  @Override
  public ReadWriteLock getReadWriteLock() {return null;}

  @Override
  public void putObject(Object key, Object object) {entriesToAddOnCommit.put(key, object);
  }

  @Override
  public Object removeObject(Object key) {return null;}

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();}

  public void commit() {if (clearOnCommit) {delegate.clear();
    }
    flushPendingEntries();
    reset();}

  public void rollback() {unlockMissedEntries();
    reset();}

  private void reset() {
    //1. 是否革除提交
    clearOnCommit = false;
    //2. 暂存区清理, 代表这个事务从头开始做了,之前的清理掉
    entriesToAddOnCommit.clear();
    //3. 同上
    entriesMissedInCache.clear();}
    
  /** 
   * 将暂存区的数据提交到缓存中
   **/
  private void flushPendingEntries() {for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {delegate.putObject(entry.getKey(), entry.getValue());
    }
    // 如果缓存中不蕴含这个 key, 就将 key 对应的 value 设置为默认值 null
    for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);
      }
    }
  }

  // 移除缺失的 key, 就是这个缓存中没有的 key 都移除掉
  private void unlockMissedEntries() {for (Object entry : entriesMissedInCache) {
      try {delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
            + "Consider upgrading your cache adapter to the latest version.  Cause:" + e);
      }
    }
  }

}

2.10 WeakCache

从名字上看跟 SoftCache 有点关系,Soft 援用是当内存不够用时候才清理, 而Weak 弱援用则相同, 只有有 GC 就会回收。所以他们的类型个性并不是本人实现的,而是依赖于 Reference<T> 类的个性,所以代码就不看了根本和 SoftCache 实现一摸一样。

感谢您的浏览,本文由 西魏陶渊明 版权所有。如若转载,请注明出处:西魏陶渊明(https://blog.springlearn.cn/)

本文由 mdnice 多平台公布

正文完
 0