mybatis 之缓存接口
为啥使用缓存?
这是一个值得思考清楚的问题, 为了能够加快访问速度,提高系统的吞吐量。
使用缓存带来的问题?
缓存一致性问题, 即缓存是否与最新的数据保持一致。
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 之缓存接口相关推荐
- 通过源码分析MyBatis的缓存
前方高能! 本文内容有点多,通过实际测试例子+源码分析的方式解剖MyBatis缓存的概念,对这方面有兴趣的小伙伴请继续看下去~ MyBatis缓存介绍 首先看一段wiki上关于MyBatis缓存的介绍 ...
- 细说Mybatis一级缓存、二级缓存以及mybatis获取mapper的面向接口编程思想(Mapper接口动态代理实现原理)(二)
上一章和大家分享了Mybatis一级缓存和二级缓存,本章将继续和大家分享Mapper接口动态代理实现原理,按照国际惯例,先看源码,然后结合原理,写一个自己的小demo,从理论到实战,真正掌握面向接口编 ...
- 深入了解MyBatis二级缓存
深入了解MyBatis二级缓存 一.创建Cache的完整过程 我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始: Reader reader ...
- springboot mybatis 事务_SpringBoot 下 Mybatis 的缓存
"IT魔幻屋"致力于让你遇见更好的自己! 说起 mybatis,作为 Java 程序员应该是无人不知,它是常用的数据库访问框架.与 Spring 和 Struts 组成了 Java ...
- mybatis的缓存机制(一级缓存二级缓存和刷新缓存)和mybatis整合ehcache
1 查询缓存 1.1 什么是查询缓存 mybatis提供查询缓存,用于减轻数据压力,提高数据库性能. mybaits提供一级缓存,和二级缓存. 一级缓存是SqlSession级别的缓存.在 ...
- Mybatis一级缓存,二级缓存的实现就是这么简单
介绍 又到了一年面试季,所以打算写一点面试常问的东西,争取说的通俗易懂.面试高级岗,如果你说熟悉Mybatis,下面这些问题基本上都会问 Mybatis插件的实现原理? 如何写一个分页插件? Myba ...
- (Mybatis)缓存
文章目录 1.简介 2.Mybatis缓存 3.一级缓存 4.二级缓存 1.简介 什么是缓存 [ Cache ]? 存在内存中的临时数据. 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用 ...
- (转)mybatis一级缓存二级缓存
一级缓存 Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言.所以在参数和SQL完全一样的情况下,我们使用同一个SqlSess ...
- Mybatis一级缓存、二级缓存
一级缓存:SqlSession mybatis一级缓存是指在内存中开辟一块区域,用来保存用户对数据库的操作信息(sql)和数据库返回的数据,如果下一次用户再执行相同的请求,那么直接从内存中读数数据而不 ...
最新文章
- const引用和非const引用
- 快速上手RaphaelJS--RaphaelJS_Starter翻译(二)
- codeforces 拼手速题2
- window应用移植到Linux下(应用移植)
- 怎么让电脑不自动休眠_【平安惠阳提醒您】电脑应设置自动休眠 避免产生火灾隐患...
- 感知算法论文(四):Mask Scoring R-CNN (2019)译文
- GitHub托管BootStrap资源汇总(持续更新中…)
- Mybatis MySQL批量更新
- centos java创建文件_CentOS java生成文件并赋予权限的问题
- python资料-(转)python资料汇总(建议收藏)零基础必看
- 【搞事情】英文文档单词对比自动翻译
- 【收藏】10个重要问题概览Transformer全部内容
- System.Timers.Timer 与 System.Threading.Timer 小间隔
- 在SharePoint 2013中显示“以其他用户身份登录”
- android中的MotionEvent 及其它事件处理
- Unity 手动下载汉化包并安装
- python 经典图书排行榜_书榜 | 计算机书籍(3.30-4.5)销售排行榜
- 2013年深圳百公里徒步感悟
- redis数据结构分析-redisObject-SDS
- 数量遗传学 第二章 群体的遗传组成
热门文章
- C#中 如何关联键盘按钮 (KeyChar/KeyCode值 KeyPress/KeyDown事件 区别)
- 一首好听的摇滚歌曲(Ever Dream),以及优美的译作
- R语言访问数据框某一列的特定元素
- Python 命令行进度条
- c# 异步回调post请求http
- jQuery deffered和promise对象方法
- 通过一个微信小程序商城一步步来学习小程式开发
- 文心一言 VS 讯飞星火 VS chatgpt (42)-- 算法导论5.4 6题
- 适配针式打印机EPSON爱普生,在vue项目中用原生js搭配iframe完成唤起打印弹窗
- 南京师范大学地信/GIS/地图学与地理信息系统考研经验