本地缓存 Guava Cache + Caffeine Cache

为什么我们要使用本地缓存?

  1. 空间换时间-消耗内存空间提升速度
  2. 某些热key重复hit到很多次
  3. 缓存的总容量不会超过内存的总量

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区

1
1


图片来源

https://juejin.im/post/5b8df63c6fb9a019e04ebaf4
https://www.jianshu.com/p/38bd5f1cf2f2

参考文献

https://juejin.im/post/5b8df63c6fb9a019e04ebaf4
https://www.jianshu.com/p/38bd5f1cf2f2

原文地址:https://yolkpie.net/2020/02/04/本地缓存/

posted @ 2021-02-02 15:47  yolkpie  阅读(137)  评论(0编辑  收藏  举报