本地缓存 Guava Cache + Caffeine Cache
为什么我们要使用本地缓存?
- 空间换时间-消耗内存空间提升速度
- 某些热key重复hit到很多次
- 缓存的总容量不会超过内存的总量
GuavaCache
构造
LoadingCache<String, String> build = CacheBuilder.newBuilder()
// key的最大数量
.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) {
// log do something
}
})
.build(new CacheLoader<String, String>() {
// 使用CacheLoader来动态加载数据
@Override
public String load(String s) throws Exception {
// invoke method
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时再去指定,这样比较第一条稍微灵活一些
loadingCache.get(key, new Callable<String>() {
@Override
public String call() throws Exception {
// invoke method
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处理被移除的数据
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) {
// 如果是true的情况 那么直接读数据返回 读只需要更新accessQueue队列即可
this.recordLockedRead(e, now);
var15 = entryValue;
return var15;
}
// 直接替换旧值
++this.modCount;
// 放入移除提醒队列 随后统一交给LocalCache注册的RemovalListener
this.enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
// 存入值 并且放入accessQueue和WriteQueue
this.setValue(e, key, value, now);
// 淘汰策略
this.evictEntries(e);
var15 = entryValue;
return var15;
}
// 获取的值为空的话 则直接替换
++this.modCount;
if(valueReference.isActive()) {
// 放入移除提醒队列,随后统一交给LocalCache注册的RemovalListener
this.enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);
// 存入值 并且放入accessQueue和WriteQueue
this.setValue(e, key, value, now);
newCount = this.count;
} else {
// 可能会被load后的值覆盖掉这样count减1
this.setValue(e, key, value, now);
newCount = this.count + 1;
}
this.count = newCount;
this.evictEntries(e);
var15 = null;
return var15;
}
}
// 创建新entry 放入table
++this.modCount;
e = this.newEntry(key, hash, first);
// 存入值 放入accessQueue和WriteQueue
this.setValue(e, key, value, now);
// table也存入entry
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.剩下的最后一种情况为,命中数据,直接返回
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);
// 刷新机制获取value
Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
return var17;
}
LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
if(valueReference.isLoading()) {
// loading 状态 线程阻塞等待 等值
Object var9 = this.waitForLoadingValue(e, key, valueReference);
return var9;
}
}
}
var15 = this.lockedGetOrLoad(key, hash, loader);
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;
// 首先对segment加锁
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()) {
// 如果数据正在loading 阻塞等待结果
createNewEntry = false;
} else {
V value = valueReference.get();
if(value == null) {
// 放入移除提醒队列 随后统一交给LocalCache注册的RemovalListener
this.enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
} else {
if(!this.map.isExpired(e, now)) {
// entry没有过期 命中 直接返回
this.recordLockedRead(e, now);
this.statsCounter.recordHits(1);
Object var16 = value;
return var16;
}
// 过期数据 cause是expired 放入移除提醒队列
this.enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
}
// 从writeQueue和accessQueue 移除过期无用的数据
this.writeQueue.remove(e);
this.accessQueue.remove(e);
this.count = newCount;
}
break;
}
}
if(createNewEntry) {
loadingValueReference = new LocalCache.LoadingValueReference();
if(e == null) {
// 新建一个entry的数据 将ValueReference设置为loadingValueReference 并不是完全的状态 还要调用下面的方法
e = this.newEntry(key, hash, first);
e.setValueReference(loadingValueReference);
table.set(index, e);
} else {
e.setValueReference(loadingValueReference);
}
}
} finally {
this.unlock();
this.postWriteCleanup();
}
// 将值加载到entry上
if(createNewEntry) {
Object var9;
try {
// 只需要锁entry 不需要锁segment
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