为啥使用缓存?

这是一个值得思考清楚的问题, 为了能够加快访问速度,提高系统的吞吐量。

使用缓存带来的问题?

缓存一致性问题, 即缓存是否与最新的数据保持一致。

mybatis的缓存

mybatis作为一个ORM框架, 数据的通过JDBC从数据库读取, 读取是需要进行网络通信IO,以及磁盘IO,为了提升速度,势必有必要进行缓存到内存。就比如查询操作, 如果是对同一个缓存key进行操作, 那就没必要每次都去查数据库, 直接走缓存。

mybatis的缓存设计体系

mybatis的缓存分为一级缓存和二级缓存。 访问的时候先走二级缓存, 在走一级缓存。 一级缓存一直存在, 二级缓存默认开启, 但是需要给MapperStatement设置开启标识。

mybatis的缓存接口如下

public interface Cache {/*** @return The identifier of this cache*/String getId();/*** @param key*          Can be any object but usually it is a {@link CacheKey}* @param value*          The result of a select.*/void putObject(Object key, Object value);/*** @param key*          The key* @return The object stored in the cache.*/Object getObject(Object key);/*** As of 3.3.0 this method is only called during a rollback* for any previous value that was missing in the cache.* This lets any blocking cache to release the lock that* may have previously put on the key.* A blocking cache puts a lock when a value is null* and releases it when the value is back again.* This way other threads will wait for the value to be* available instead of hitting the database.*** @param key*          The key* @return Not used*/Object removeObject(Object key);/*** Clears this cache instance.*/void clear();/*** Optional. This method is not called by the core.** @return The number of elements stored in the cache (not its capacity).*/int getSize();/*** Optional. As of 3.2.6 this method is no longer called by the core.* <p>* Any locking needed by the cache must be provided internally by the cache provider.** @return A ReadWriteLock*/default ReadWriteLock getReadWriteLock() {return null;}}

缓存接口实现类分析

首先得知道mybatis的缓存设置整体上使用了装饰器设计模式 也有委托者模式的影子。

PerpetualCache

缓存说到底就是把数据存储在内存里面, 那么必定会有一个数据结构用来存储的,所以这个PerpetualCache 既是缓存功能实现的基石。

public class PerpetualCache implements Cache { //持久化缓存private final String id;private final Map<Object, Object> cache = new HashMap<>(); //使用hashMap作为缓存public PerpetualCache(String id) {this.id = id;}@Overridepublic String getId() {return id;}@Overridepublic int getSize() {return cache.size();}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}@Overridepublic 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()); //使用id进行判断}@Overridepublic int hashCode() {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}return getId().hashCode();}}

关键也就是使用了HashMap作为缓存的存储结构。

LruCache

缓存, 如果没有置换算法,时不能一直往里面加的,比如我设置缓存大小为10M, 那么 但要达到10M,就必须采取换粗置换算法,把一些不值得的数据用新的覆盖掉, LruCache采用了最近最久未使用的算来淘汰, 当然还有一个比较出名的最近最少使用。 Mybatis默认提供了前者。 大家也可以想一下, 用户什么数据结构实现比较好。链表

为啥呢, 想一下缓存key一直put, 如果使用数组来维护, 数据就得一直扩容,所以使用链表比较号,不管时使用数据还是使用链表, 一个问题就遍历查找效率都比较低, 所以需要使用hash表来提升查找速度,这样我们就可以想到HashMap, 但是我们还需要记录添加的先后顺序,所以想到LinkedHashMap。

熟悉HashMap代码的, 应该都会留意HashMap的插入过程留下很多的模板方法,提供一些功能扩展。

LinkedHashMap, 最大的特点就是说期维护了一个前后插入关系的链表结构,要知道HashMap的读取的时候是和插入的时候的顺序无关。

那LinkedHashMap怎么做到呢?

static class Entry<K,V> extends HashMap.Node<K,V> {Entry<K,V> before, after; //LinkedhashMap为啥有序的原因Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}

上图为LinkedHashMap的Entry节点, 其继承了HashMap的Node节点。
补充了 before, after 引用, 这也就说明了LinkedHashMap如何做到有序的。

另一个问题就在那些地方维护这样一个before、after结构呢?

在HashMap的put过程中, 主要是两种情况, 第一种就是put的元素key在HashMap不存在,属于新插入的,所以在这种情况下 Before、After的结构 采用尾巴插法,维护先后关系。 第二种情况下就是 插入的key在HashMap中是存在的,根据HashMap的代码扩展模板方法, 可以看见LinkedHashMap在这种情况下是在afterNodeAccess(Node)方法中维护了 这个Before、After关系。

前面说了这么多, 那这和LRU有啥关系呢?
既然数据已经存入了,那么现在只需要把最久为使用的节点拿掉就行。
这部分逻辑在模板方法afterNodeInsertion(evict)中实现。

 void afterNodeInsertion(boolean evict) { // possibly remove eldestLinkedHashMap.Entry<K,V> first;if (evict && (first = head) != null && removeEldestEntry(first)) {K key = first.key;removeNode(hash(key), key, null, false, true);}}

从图中可见first指向了 最久为使用的head节点。
其中LinkedHashMap又提供了一个removeEldestEntry方法留给子类去扩展,到底要不要移除这个最久为使用的节点。

最后回到LruCache , keyMap实现了LinkedHashMap 并重写removeEldestEntry方法, 设置了如果LinkedHashMap的数据量大小大于设置的size,那么久进行移除。 并记录这要移除的key,用来移除缓存。

private void cycleKeyList(Object key) {keyMap.put(key, key); //lruif (eldestKey != null) {//存不存在老keydelegate.removeObject(eldestKey);//缓存移除eldestKey = null;}}

BlockingCache

当在缓存中找不到元素时,它在缓存键上设置一个锁。这样,其他线程将等待该元素被填满,而不是访问数据库。根据其性质,如果使用不当,此实现可能导致死锁。

public class BlockingCache implements Cache {private long timeout;private final Cache delegate;private final ConcurrentHashMap<Object, CountDownLatch> locks;//呃,这个也太离谱了}

其数据结构上述代码所示, timeout 为等待锁的超时时间, delegate为委托的另外缓存,locks 为map结构key为缓存key ,value为锁对象, 有趣的是这个锁对象使用的是CountDownLatch,

添加缓存
添加缓存, 并不控制,只能由一个线程去给当前key添加, 当缓存添加之后需要释放锁,缓存所有等待该缓存的线程。

  private void releaseLock(Object key) {CountDownLatch latch = locks.remove(key);//获取该缓存key的 倒计数器if (latch == null) { //这种情况不应该发生throw new IllegalStateException("Detected an attempt at releasing unacquired lock. This should never happen.");}latch.countDown(); //CountDownLatch 用法}

获取缓存

public Object getObject(Object key) {acquireLock(key);//尝试获取锁Object value = delegate.getObject(key);if (value != null) {releaseLock(key); //释放锁}return value;}

获取锁的方法

private void acquireLock(Object key) {CountDownLatch newLatch = new CountDownLatch(1); //创建一个倒计数器while (true) {CountDownLatch latch = locks.putIfAbsent(key, newLatch); //如果key不存在那么我就把当前的到计数器塞入 并发map, 存在那么返回原先的到计数器if (latch == null) { //说明我是第一个访问该key的break;//那我就不用阻塞}try {if (timeout > 0) { //设置了超时时间boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS); //等待if (!acquired) { //没有获取锁throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());}} else {latch.await(); //一直阻塞}} catch (InterruptedException e) { //中断异常throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);}}}

上述逻辑 所有的缓存key都会公用一个CounDownLatch, 并由添加该缓存key是countDown释放锁, 但是我是感觉有个bug的就是, 第一个该key的缓存时不会被锁住的。

LoggingCache

这个就比较简单了, 一个日志缓存命中的装饰器。
数据结构为:

public class LoggingCache implements Cache { //日志缓存private final Log log; //日志private final Cache delegate;protected int requests = 0; //请求次数protected int hits = 0; //命中次数}

具体计算命中百分比:

@Overridepublic Object getObject(Object key) {requests++; //请求次数加1final Object value = delegate.getObject(key);if (value != null) {hits++; //命中次数加一}if (log.isDebugEnabled()) { //输出日志log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());}return value;}

FifoCache

先进先出的缓存装饰器
既然时先进先出, 那么一个比较合适的数据结构就是队列。
所以FIfoCache的数据结构如下:

public class FifoCache implements Cache {private final Cache delegate;private final Deque<Object> keyList; //队列数据结构private int size; //队列大小}

缓存淘汰算法关键代码:

private void cycleKeyList(Object key) {keyList.addLast(key); //队列中为末尾添加缓存keyif (keyList.size() > size) { //如果达到了大小限制Object oldestKey = keyList.removeFirst(); //移除最先进入的换粗keydelegate.removeObject(oldestKey); //移除缓存}}

整个装饰器并没有考虑线程安全问题, 所以必须来一个线程安全的装饰器。

SynchronizedCache

整个缓存装饰器, 的所有cache接口方法,在实现的时候都加了Synchronized。

ScheduledCache

这个装饰也很简单, 就是设置一个清理的间隔, 每次调用自己的方法都会去判断到了清理的时间间隔了没有,需要清理就会调用委托缓存的清理方法。

public class ScheduledCache implements Cache { //定时清理缓存private final Cache delegate;protected long clearInterval;protected long lastClear;}
private boolean clearWhenStale() {if (System.currentTimeMillis() - lastClear > clearInterval) {//判断是否到了清理周期clear();return true;}return false;}

SerializedCache

序列换缓存value的装饰器

添加缓存

  public void putObject(Object key, Object object) {if (object == null || object instanceof Serializable) { //判断value是否支持缓存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();return bos.toByteArray();} catch (Exception e) {throw new CacheException("Error serializing object.  Cause: " + e, e);}}

反序列化方法

private Serializable deserialize(byte[] value) {SerialFilterChecker.check();Serializable result;try (ByteArrayInputStream bis = new ByteArrayInputStream(value);ObjectInputStream ois = new CustomObjectInputStream(bis)) {result = (Serializable) ois.readObject();} catch (Exception e) {throw new CacheException("Error deserializing object.  Cause: " + e, e);}return result;}

WeakCache

弱引用的缓存,当JVM进行垃圾回收的时候就会把弱引用进行垃圾回收。

数据结构:

public class WeakCache implements Cache {private final Deque<Object> hardLinksToAvoidGarbageCollection; //强引用避免垃圾回收private final ReferenceQueue<Object> queueOfGarbageCollectedEntries; //引用队列private final Cache delegate;private int numberOfHardLinks; //强引用数量限制
}

进行putObject

  @Overridepublic void putObject(Object key, Object value) {removeGarbageCollectedItems(); //清理弱引用队列里面的元素delegate.putObject(key, new WeakEntry(key, value, queueOfGarbageCollectedEntries)); //添加弱引用, 并指定弱引用队列}

getObject

@Overridepublic Object getObject(Object key) {Object result = null;@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cacheWeakReference<Object> weakReference = (WeakReference<Object>) delegate.getObject(key);if (weakReference != null) {result = weakReference.get(); //说明已经进行一次垃圾回收, 已经放入引用队列if (result == null) {delegate.removeObject(key); //移除} else { //次缓存被外交获取, 应该是比较重要的缓存,故添加强引用,避免垃圾回收synchronized (hardLinksToAvoidGarbageCollection) {hardLinksToAvoidGarbageCollection.addFirst(result);if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { //FIFO 缓存淘汰hardLinksToAvoidGarbageCollection.removeLast();}}}}return result;}

SoftCache

软引用缓存装饰器 , 和弱引用装饰器差不多,区别就是在于,软引用时FULLGC时候才会把引用对象移动到引用队列。

总结

简要的介绍了 , Mybatis缓存组件, 我们可以自己实现缓存Cache接口, 然后让Mybatis使用我们的缓存实现。

mybatis 之缓存接口相关推荐

  1. 通过源码分析MyBatis的缓存

    前方高能! 本文内容有点多,通过实际测试例子+源码分析的方式解剖MyBatis缓存的概念,对这方面有兴趣的小伙伴请继续看下去~ MyBatis缓存介绍 首先看一段wiki上关于MyBatis缓存的介绍 ...

  2. 细说Mybatis一级缓存、二级缓存以及mybatis获取mapper的面向接口编程思想(Mapper接口动态代理实现原理)(二)

    上一章和大家分享了Mybatis一级缓存和二级缓存,本章将继续和大家分享Mapper接口动态代理实现原理,按照国际惯例,先看源码,然后结合原理,写一个自己的小demo,从理论到实战,真正掌握面向接口编 ...

  3. 深入了解MyBatis二级缓存

    深入了解MyBatis二级缓存 一.创建Cache的完整过程 我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始: Reader reader ...

  4. springboot mybatis 事务_SpringBoot 下 Mybatis 的缓存

    "IT魔幻屋"致力于让你遇见更好的自己! 说起 mybatis,作为 Java 程序员应该是无人不知,它是常用的数据库访问框架.与 Spring 和 Struts 组成了 Java ...

  5. mybatis的缓存机制(一级缓存二级缓存和刷新缓存)和mybatis整合ehcache

    1      查询缓存 1.1  什么是查询缓存 mybatis提供查询缓存,用于减轻数据压力,提高数据库性能. mybaits提供一级缓存,和二级缓存. 一级缓存是SqlSession级别的缓存.在 ...

  6. Mybatis一级缓存,二级缓存的实现就是这么简单

    介绍 又到了一年面试季,所以打算写一点面试常问的东西,争取说的通俗易懂.面试高级岗,如果你说熟悉Mybatis,下面这些问题基本上都会问 Mybatis插件的实现原理? 如何写一个分页插件? Myba ...

  7. (Mybatis)缓存

    文章目录 1.简介 2.Mybatis缓存 3.一级缓存 4.二级缓存 1.简介 什么是缓存 [ Cache ]? 存在内存中的临时数据. 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用 ...

  8. (转)mybatis一级缓存二级缓存

    一级缓存 Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言.所以在参数和SQL完全一样的情况下,我们使用同一个SqlSess ...

  9. Mybatis一级缓存、二级缓存

    一级缓存:SqlSession mybatis一级缓存是指在内存中开辟一块区域,用来保存用户对数据库的操作信息(sql)和数据库返回的数据,如果下一次用户再执行相同的请求,那么直接从内存中读数数据而不 ...

最新文章

  1. const引用和非const引用
  2. 快速上手RaphaelJS--RaphaelJS_Starter翻译(二)
  3. codeforces 拼手速题2
  4. window应用移植到Linux下(应用移植)
  5. 怎么让电脑不自动休眠_【平安惠阳提醒您】电脑应设置自动休眠 避免产生火灾隐患...
  6. 感知算法论文(四):Mask Scoring R-CNN (2019)译文
  7. GitHub托管BootStrap资源汇总(持续更新中…)
  8. Mybatis MySQL批量更新
  9. centos java创建文件_CentOS java生成文件并赋予权限的问题
  10. python资料-(转)python资料汇总(建议收藏)零基础必看
  11. 【搞事情】英文文档单词对比自动翻译
  12. 【收藏】10个重要问题概览Transformer全部内容
  13. System.Timers.Timer 与 System.Threading.Timer 小间隔
  14. 在SharePoint 2013中显示“以其他用户身份登录”
  15. android中的MotionEvent 及其它事件处理
  16. Unity 手动下载汉化包并安装
  17. python 经典图书排行榜_书榜 | 计算机书籍(3.30-4.5)销售排行榜
  18. 2013年深圳百公里徒步感悟
  19. redis数据结构分析-redisObject-SDS
  20. 数量遗传学 第二章 群体的遗传组成

热门文章

  1. C#中 如何关联键盘按钮 (KeyChar/KeyCode值 KeyPress/KeyDown事件 区别)
  2. 一首好听的摇滚歌曲(Ever Dream),以及优美的译作
  3. R语言访问数据框某一列的特定元素
  4. Python 命令行进度条
  5. c# 异步回调post请求http
  6. jQuery deffered和promise对象方法
  7. 通过一个微信小程序商城一步步来学习小程式开发
  8. 文心一言 VS 讯飞星火 VS chatgpt (42)-- 算法导论5.4 6题
  9. 适配针式打印机EPSON爱普生,在vue项目中用原生js搭配iframe完成唤起打印弹窗
  10. 南京师范大学地信/GIS/地图学与地理信息系统考研经验