为什么我们要使用本地缓存?
- 空间换时间-消耗内存空间提升速度
- 某些热key重复hit到很多次
- 缓存的总容量不会超过内存的总量
GuavaCache
构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| LoadingCache<String, String> build = CacheBuilder.newBuilder()
.maximumSize(1000L)
.concurrencyLevel(10)
.expireAfterWrite(10, TimeUnit.SECONDS)
.expireAfterAccess(15, TimeUnit.MINUTES)
.recordStats()
.removalListener(new RemovalListener<String, String>() { public void onRemoval(RemovalNotification<String, String> removalNotification) { } }) .build(new CacheLoader<String, String>() { @Override public String load(String s) throws Exception { return ""; } });
|
全部队列
特征
1.结构大体和concurrentHashMap一致,但是concurrentHashMap只能显示地去移除因素,而Guava Cache能自动去回收元素
2.当缓存数据超过预先设定的最大值时,利用LRU算法去移除数据,关于LRU算法的实现:https://blog.csdn.net/caoshangpa/article/details/78783749
3.设置recordStats能统计缓存的命中率,未命中率和异常率
4.不同引用级别的key value
结构
数据的加载
1.初始化时使用CacheLoader来加载数据
2.利用回调函数,实现callable接口,在get时再去指定,这样比较第一条稍微灵活一些
1 2 3 4 5 6 7
| loadingCache.get(key, new Callable<String>() { @Override public String call() throws Exception { return ""; } });
|
软引用和弱引用
1.强引用指的是Object object = new Object(); 只有有引用 垃圾回收不会回收对象
2.软引用:CacheBuilder.newBuilder().softValues() 当要发生内存溢出时,会回收该value
3.弱引用:CacheBuilder.newBuilder().weakKeys() 当gc时就会直接回收
其他使用注意点
1.使用CacheLoader来加载数据,返回null的情况会抛异常,解决办法可以为对cache.get()加trycatch处理
2.cache的超时机制是不准确的,设定存活60秒,可能为61秒或者62秒
3.如果不用load去动态加载数据,直接用cache.getIfPresent就可以了,有值则返回,无值则返回null
4.调用invalid和invalidAll来删除缓存,也可以手动用cleanUp来回收缓存
5.maximumWeight用改值可以设定缓存的最大权重,超过后采用淘汰策略LRU
6.cache.asMap将缓存转化为ConccurentHashmap
put
1.先根据当前时间用preWriteCleanup来清理无效的数据
2.不能找到entry的情况下,直接创建新的entry,储存数据,更新ccessQueue和WriteQueue
3.找到entry的情况下,如果为空,直接更新新值,更新ccessQueue和WriteQueue
4.找到entry的情况下,如果不为空,根据onlyIfAbsent来判断,true的情况直接读旧的数据,更新accessQueue,false的情况直接更新新值,更新ccessQueue和WriteQueue,然后直接返回旧数据
5.用postWriteCleanup处理被移除的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| this.lock();
try { long now = this.map.ticker.read(); this.preWriteCleanup(now); int newCount = this.count + 1; if(newCount > this.threshold) { this.expand(); newCount = this.count + 1; }
AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table; int index = hash & table.length() - 1; ReferenceEntry<K, V> first = (ReferenceEntry)table.get(index);
ReferenceEntry e; Object entryKey; for(e = first; e != null; e = e.getNext()) { entryKey = e.getKey(); if(e.getHash() == hash && entryKey != null && this.map.keyEquivalence.equivalent(key, entryKey)) { LocalCache.ValueReference<K, V> valueReference = e.getValueReference(); V entryValue = valueReference.get(); Object var15; if(entryValue != null) { if(onlyIfAbsent) { this.recordLockedRead(e, now); var15 = entryValue; return var15; } ++this.modCount; this.enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED); this.setValue(e, key, value, now); this.evictEntries(e); var15 = entryValue; return var15; } ++this.modCount; if(valueReference.isActive()) { this.enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED); this.setValue(e, key, value, now); newCount = this.count; } else { this.setValue(e, key, value, now); newCount = this.count + 1; }
this.count = newCount; this.evictEntries(e); var15 = null; return var15; } } ++this.modCount; e = this.newEntry(key, hash, first); this.setValue(e, key, value, now); table.set(index, e); newCount = this.count + 1; this.count = newCount; this.evictEntries(e); entryKey = null; return entryKey; } finally { this.unlock(); this.postWriteCleanup(); }
|
get
1.进入segment,进入table,如果直接命中调用scheduleRefresh返回结果
2.如果没有命中,进入后发现正在loading(其他线程加载数据状态),则线程阻塞,等待加载的结果
3.前两条都不符合,那就是说这个entry为null,还没有拿到数据调用lockedGetOrLoad方法获取数据
4.进入lockedGetOrLoad后,首先对segment加锁,如果没有找不到这个entry,创建新的entry,同步加载Vlaue到entry中
5.进入lockedGetOrLoad后,如果找到了这个entry,如果正在loading了,那就等待结果
6.进入lockedGetOrLoad后,如果找到了这个entry,否则如果value为null,证明被gc,或者是过期了的数据,那么这两种移入移除提醒队列,并且在writeQueue和accessQueue中remove掉,然后创建新的entry,同步加载Vlaue到entry中
7.剩下的最后一种情况为,命中数据,直接返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if(this.count != 0) { ReferenceEntry<K, V> e = this.getEntry(key, hash); if(e != null) { long now = this.map.ticker.read(); V value = this.getLiveValue(e, now); if(value != null) { this.recordRead(e, now); this.statsCounter.recordHits(1); Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader); return var17; }
LocalCache.ValueReference<K, V> valueReference = e.getValueReference(); if(valueReference.isLoading()) { Object var9 = this.waitForLoadingValue(e, key, valueReference); return var9; } } }
var15 = this.lockedGetOrLoad(key, hash, loader);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException { LocalCache.ValueReference<K, V> valueReference = null; LocalCache.LoadingValueReference<K, V> loadingValueReference = null; boolean createNewEntry = true; this.lock();
ReferenceEntry e; try { long now = this.map.ticker.read(); this.preWriteCleanup(now); int newCount = this.count - 1; AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table; int index = hash & table.length() - 1; ReferenceEntry<K, V> first = (ReferenceEntry)table.get(index);
for(e = first; e != null; e = e.getNext()) { K entryKey = e.getKey(); if(e.getHash() == hash && entryKey != null && this.map.keyEquivalence.equivalent(key, entryKey)) { valueReference = e.getValueReference(); if(valueReference.isLoading()) { createNewEntry = false; } else { V value = valueReference.get(); if(value == null) { this.enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED); } else { if(!this.map.isExpired(e, now)) { this.recordLockedRead(e, now); this.statsCounter.recordHits(1); Object var16 = value; return var16; } this.enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED); } this.writeQueue.remove(e); this.accessQueue.remove(e); this.count = newCount; } break; } }
if(createNewEntry) { loadingValueReference = new LocalCache.LoadingValueReference(); if(e == null) { e = this.newEntry(key, hash, first); e.setValueReference(loadingValueReference); table.set(index, e); } else { e.setValueReference(loadingValueReference); } } } finally { this.unlock(); this.postWriteCleanup(); } if(createNewEntry) { Object var9; try { synchronized(e) { var9 = this.loadSync(key, hash, loadingValueReference, loader); } } finally { this.statsCounter.recordMisses(1); }
return var9; } else { return this.waitForLoadingValue(e, key, valueReference); } }
|
CaffeineCache
介绍
封装的API和Guava Cache基本上相同
在spring5.0以后使用了Caffeine代替了Guava Cache,因为Caffeine的性能比Guava Cache强很多,并且它的理想命中率也比Guava Cache高很多,这要得益于他的W-TinyLFU算法,那么W-TinyLFU又是什么呢,首先我们知道LRU是淘汰最久的数据,那么对于突发的的访问量旧的数据就被冲掉,LFU是代表访问频率最小的被淘汰掉,但是有些数据只是一段时间会访问到,所以在垃圾数据会被挤压太久,所以两者都有缺陷,这样W-TinyLFU可以说是LFU和LRU的完美结合,具体如下图所示
- 新数据进来首先进入Eden区,那么就算有突发的访问频率,也不会把这些数据给冲掉
- 被访问过一次以上数据会从Eden进入Probation,Probation称为缓刑区,一旦没有晋升到Protected,那么这个区的数据最有可能先死
- 从Probation的数据再被访问时会进入Protected区,在这个区称为保护区,这里的数据相对安全,但是一旦Protected区数据满了,被淘汰的数据,又会回到Probation区
图片来源
https://juejin.im/post/5b8df63c6fb9a019e04ebaf4
https://www.jianshu.com/p/38bd5f1cf2f2
参考文献
https://juejin.im/post/5b8df63c6fb9a019e04ebaf4
https://www.jianshu.com/p/38bd5f1cf2f2