本地缓存

缓存大概是一个不能再熟悉的话题,今天的主要内容是分享一下我们公司使用比较多的本地缓存 loadingcache,同时也是自己使用过程中的一些探索的分享,其背后的架构其实就是Guava cache,Guava Cache 是一个全内存的本地缓存实现,它提供了线程安全的实现机制。 整体上来说Guava Cache 是本地缓存的不二之选。

在说到本地缓存之前我可能有一个问题想问一下大家 ,想问大家如何理解缓存,缓存的作用的是什么?

缓存可以提高系统的性能,减少对应的请求时间,其实我理解的缓存的本质就是一个用空间换时间的一个思想。

提供“缓存”的目的是为了让数据访问的速度适应CPU的处理速度,其基于的原理是内存中“局部性原理”,在程序的执行期间,处理器的指令访问存储和数据的访问存储呈现“簇状”,典型的程序包括许多迭代循环和子程序,一旦程序进入到一个循环或者是子程序执行,就会重复访问一个小范围的指令集合,经过很长的一段时间,程序访问的“簇”会改变,但是在短时间内,处理器主要访问存储器中固定的“簇”,即一定程序执行时间和空间内,被访问的代码集中于一部分。

CPU 缓存的是内存数据,用于解决 CPU 处理速度和内存不匹配的问题比如处理器和内存之间的高速缓存,操作系统在内存管理上针对虚拟内存方案为页表项使用了一特殊的高速缓存TLB,转换检测缓冲区,因为每个虚存访问会引起两次物理访问,一次取相关的页表项,一次取数据,TLB引入来加速虚拟地址到物理地址的转换。

回归到业务来说:我们为了减轻数据库的压力、提升用户的体验、提高系统并发量,所以我们在数据库之上增加了缓存这一层来弥补。接下来主要分享自己在使用过程中的一个探索,相关的一些原理和使用场景。

1.loadingcache相关数据结构


背后结构被ConcurrentHashMap,所以其是一个K.V的存储结构,这个图我想应很明显了,这分明就就是ConcurrentHashMap的结构,底层是一个segment数组,链表的节点和ConcurrentHashMap不太一样,是一个每一个segment是一个节点为ReferenceEntry<K, V>数组,segment继承了ReentrantLock,缩小了锁的力度,体现了分段式锁的思想。

节点ReferenceEntry是是一个顶部接口,由相关具体的实现类承接,最终节点的成员就是
{int hash
ValueReference<K, V> valueReference
T referent
ReferenceEntry<K, V> next
}
这里顺便提一下LoadingValueReference<K,V>
LoadingValueReference<K, V> implements LocalCache.ValueReference<K, V>
不同点在于判断isloading的时候默认返回true,ValueReference默认返回false。

2.主要介绍一下平常用的几种设置方式

设置缓存过期时间 loadingCache没有缓存自动清除概念。
expireAfterWrite: 写过期
expireAfterAccess: 读,写过期,
refreshAfterWrite:写刷新

缓存是否过期,是根据当前的时间和缓存值时间戳的差值与过期时间比较
1.expireAfterWrite:,在put或者和Load的时候更新缓存的时间戳,因为这两种操作是写操作,在get过程中如果发现当前时间与时间戳的差值大于过期时间,就会进行load操作。
2.expireAfterAccess,和上边最大的区别就是,不管是写还是读都会记录新的时间戳,所以不会很快导致缓存过期,所以当读的时候,会和最新的时间戳进行对比,最新的时间戳可能是因为写或者读而更改。
3.refreshAfterWrite, 再调用get进行值的获取的时候才会执行reload操作,如果缓存项没有被检索,那么刷新就不会真正的存在。这里的刷新操作需要我们去设置一个cacheLoader,去自行实现load法,但事实上如果想要进行异步刷新需要重写cacheLoader的reload方法,因为在经刷新的时候调用的是reload的方法。

我们公司经常用到的异步加载的cacheloader,在reload中实现了异步调用load的操作实现异步加载。当然你也可以自己写一个无伤大雅。

3.关注缓存返回值

上边说了guavacache没有自动清楚缓存的概念,
对于loadingcache来说,我们设置过期时间更过的是关注数据的可用性,但是可能会因为设置的问题使得数据的可用性降低。

下面先来说明我们再设置expireAfterAcess和expireAfterWrite这两个设置,如果缓存有这两个设置,那么在进行get的过程中,缓存失效的话,会进行load操作,load是是一个同步加载的操作。
同步等待分为两种的线程分为两种,发现loading处于等待状态,也可能是想要去load的被reeatrantLock的阻塞队里中,但是所有线程都获取的是最新值,

缓存击穿:假如在缓存过期的那一瞬间,有大量的并发请求过来。他们都会因缓存失效而去加载执行db操作,可能会给db造成毁灭性打击

采用expire相关的设置来防止缓存击穿,expire则通过一个加锁的方式,只允许一个线程去回源,有效防止了缓存击穿,但是可以从源代码看出,在有效防止缓存击穿的同时,会发现多线程的请求同样key的情况下,一部分线程在waitforvalue,而另一部分线程在reentantloack的阻塞中。

refreshAfterWrite同步加载
如果采用refresh的话,会通过scheduleRefresh方法去进行reload,同样的reload的时候只有一个线程去回源,对于其他的线程来说,不会阻塞,但是可能返回旧值可能返回新值,看起来性能比expire的性能要高一些,但是如果某个key的吞吐量很低,可能会获取到很久之前的旧
值,不太友好。
当加载缓存的线程是同步加载的话,对于其他线程来说,不会被阻塞,直接返回值,可能是旧值或者是新值,但是加载缓存的线程一定是新值。

refreshAfterWrite异步加载
当加载缓存的线程是异步加载的话,对于其他线程来说,不会被阻塞直接返回值,可能是新值或者是旧值,但是加载缓存的线程返回的不一定是新值,有可能是旧值。

4.正确使用方式

Guava Cache 并没使用额外的线程去做定时清理和加载的功能,而是依赖于查询请求。在查询的时候去比对上次更新的时间,如超过指定时间则进行回源。所以,如果使用refreshAfterWrite,在吞吐量很低的情况下,如很长一段时间内没有查询之后,发生的查询有可能会得到一个旧值(这个旧值可能来自于很长时间之前),是先判断过期,再判断refresh,所以我们可以通过设置refreshAfterWrite为1s,将expireAfterWrite设为2s,当访问频繁的时候,会在每秒都进行refresh,而当超过2s没有访问,下一次访问必须load新值。
freshTime一定要小于cacheExpiredTime才有意义
这个设置还可以解决一个场景就是,如果长时间没有访问缓存,可以保证 expire 后可以取到最新的值,而不是因为 refresh 取到旧值。

5.LRU算法

5.1.操作系统LRU算法

LRU算法是操作系统在进行内存管理过程置换策略的一种,当内存中的所有页框都被占据,并且需要读一个新页以处理一次缺页中断时候,置换策略决定当前在内存中的那个页将被置换,所有的置换策略的目标都是移出最近最不可能访问到的页,LRU算法基于分页基础之上,主要就是置换内存中上次使用距离当当前最远的页。

5.2.Guava cache LRU算法

1.说到LRU算法,可能最容易想到的就是采用数组加时间戳的方式,通过遍历来确定那个缓存项是最近最少访问,这种方式看起来比较的简单,但是访问和查找替换实收的时间复杂度为o(n).

2.采用双向链表的方式,然后每次访问某个元素就将元素移动到链表头部。这样链表头部的尾部的元素就是最近最少使用的元素。替换的复杂度为o(1),但是访问的复杂度还是O(n)。

3.loadingCache在选择存储结构的时候底层选择了concerentHashMap结构,其充分利用二者的优势,在Guava Cache的LRU实现中,它的双向链表并不是全局的,而是每个Segment,LRU中都有。其中一共涉及到三个Queue其中包括:AccessQueue和WriteQueue,以及RecentQueue。其中AccessQueue和WriteQueue就是双向链表;而RecentQueue才是真正的Queue,它就是CocurrentLinkedQueue。

接下来我们将分析Guava Cache是如何通过这三个Queue来实现的LRU。
这两个Queue(双向链表)是Guava Cache自己实现的一个比较简单的双向链表,为了性能,其设计成了非线程安全的。因此对这个两个Queue的操作就需要在获得了Segment的lock的场景下才能使用。

其中AccessQueue负责存储对元素的读取行为记录。而WriteAccess则负责对元素的写入行为进行记录即访问的时候将最近访问的节点移动到链表前面。
该实现通过HashMap来存储元素,从而解决了读写元素的时间复杂度的问题。我们都知道HashMap的时间复杂度为O(1)。当然这是理想情况,如果内存够大的话,其通过双向链表又解决了查找最少使用元素的问题,其时间复杂度仍然为O(1)。

这里有个问题为什么要存在WriteQueue,上面我们不是说了Hash+链表的实现思想只需要一个链表就OK了,为什么这里还要有WriteAccess呢?因为在Guava Cache中可以设置元素在写入后多久就被删除(即视为失效)。因此需要由WriteAccess来让元素根据写入时间排序(链表中每个节点页记录了元素的write时间)。

既然已经有了AccessQueue我们就能够知道元素的访问顺序了,从而很容易实现LRU算法了。为什么还需要RecentQueue这个CocurrentLinkedQueue呢?

因为上面我们已经提到到过,AccessQueue被设计成了线程不安全,因此必须要在获取到Segment中的Lock的时候才能访问。设想一下当我们访问元素的时候需要怎么操作才能够确保被访问的元素在AccessQueue中能够被移动到前面去?很明显为了实现着功能,我们必须在每次访问元素的时候都需要获取Segment中的Lock,然后才能够安全地将元素移动AccessQueue的前面去。这样功能我们是实现了,但是每次访问元素的时候我们都需要获取锁。这样就破坏了ConcurrentHashMap的分段锁的思想(ConcurrentHashMap分段锁思想中get是不需要获取锁的,这样才能够提供高效的读取性能),导致元素的读取变得很慢,性能很低。

因此为了确保Guava Cache的性能,它引入了RecencyQueue这个同步队列。在读取元素的时候,将所有被访问元素添加到RecencyQueue中。因为其是同步队列所以支持并发插入。这样就确保了高性能的读取能力。当在某些场景下获取到锁的时候,就再将RecencyQueue中的元素移动到AccessQueue中。

6.总结

我理解本地缓存是一个被动更新的过程,缓存在未失效的情况下,确实是保证了其可用性,却很难保证数据的正确性,传统意义上,需要等 缓存数据过期,命中缓存失败,才去DB中更新数据,导致缓存内的数据不是最新的数据,如果缓存的过期时间过长,数据的不一致的风险就越高。 如果想要及时的保证缓存与DB数据一致的话,另一种就是监听binlog,当DB中的数据发生变化的时候,触发cachesetter去更新缓存。

7.相关源码解读

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {Preconditions.checkNotNull(key);Preconditions.checkNotNull(loader);Object var15;try {//count != 0 表示segment中有数据,可以进行segment中table数据的查找,判断是否有key对应的缓存数据//count = 0,表示 segment中没有数据,在get的时候需要进行load操作,if (this.count != 0) {//getEntry方法主要是通过key,hash确定一个ReferenceEntry,相当于查找缓存数据,ReferenceEntry<K, V> e = this.getEntry(key, hash);//e == null 表示没有该缓存,直接到下面var15的逻辑if (e != null) {long now = this.map.ticker.read();//getLiveValue主要判断是否设置了expireAfterWrite或者是expireAfterAcess,并且判断是否过期。V value = this.getLiveValue(e, now);//value为null,说明过期了,不为null表示没有过期if (value != null) {this.recordRead(e, now);this.statsCounter.recordHits(1);//在这里主要进行判断,是否需要刷新,当出现了多线程的情况,第一个线程执行刷新操作的时候,主要的工作先将value变成loadingvalue,对于其他的线程来说,在进行刷新的时候判断是否证在loading的时候已经发现,正在loading,会直接返回旧值,而不是去进行同步等待,//这也是refresh和expire的区别,Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);return var17;}//value == null的情况执行下边,过期就应该重新加载,所以首先判断该值是否正在加载,//执行这里说明expire相关的设置过期,过期需要重新加载,//对于多线程的情况进行获取缓存的值,线程发现数据过期,需要重新加载,,如果当前的数据没有被其他线程加载,则直接执行lockedGetOrLoad进行同步加载的操作,对于其他线程来说,获取缓存发现过期,此时有线程进行loading,则进入到waitForLoadingValue方法进行等待LocalCache.ValueReference<K, V> valueReference = e.getValueReference();if (valueReference.isLoading()) {//同步等待,//waitForLoadingValue中的主要内容就是valueReference.waitForValue,此时已经判断值在loading,所以此时的valueReference被替换成了loadingValueReference,waitForValue获取的是loadingValueReference中的future成员,该成员在值被同步加载或者异步加载之后设置。Object var9 = this.waitForLoadingValue(e, key, valueReference);return var9;}}}//执行到这里,1,count = 0 表示segment中没有数据 2.e == null 通过key没有找到缓存的数据 3.value == null表示缓存过期,并且有其他线程在loading数据.var15 = this.lockedGetOrLoad(key, hash, loader);//lockedGetOrLoad 的主要内容就是,将valueReference替换为loadingvaluereference,使得只有一个线程加载,其他线程通过判断valueReference为loading状态就需要同步等待,然后就是真正的同步加载,调用loadSync,进行同步加载缓存值。//lockedGetOrLoad 对于多线程来说,//  1.多个线程的情况下,一个线程完整的执行lockedGetOrLoad,其他线程通过isLoading判断出正在加载,然后进行同步等待//  2.一个线程判断isloading 发现为false,时间片段到了,另一个线程判断isloading,为false,继续执行,则上一个线程重新获取执行机会的时候,会进入到lockedGetOrLoad,就会在执行lock时候进行阻塞,并且如果,第二个线程刚设置完loadingvalue,释放锁,第一线程此时可能被唤醒继续执行,如果说第二个线程的时间片段到了,不进行接下来的loadSync,此时value还处于loading状态,那么第一线程判断loading,createNewEntry = false,然后直接通过createNewEntry = false 直接进行waitForLoadingValue,也就是同步等待,如果说第二个线程同步刷新也执行完了,那么此时value肯定不属于loading状态,第一线程在被唤醒之后,判断不为loading状态,会先判断是否过期,因为第二个线程刚刷新完,所以此时肯定不会过期,所以直接返回valuereference,此时的valuereference为更新的值了,为最新值,如果说过期的话,那该线程就执行正常的同步刷新操作,就和最原始的判断缓存过期进行同步刷新操作。//所以对于expire来说,所有线程返回的都是新值,但是需要同步等待,} catch (ExecutionException var13) {Throwable cause = var13.getCause();if (cause instanceof Error) {throw new ExecutionError((Error)cause);}if (cause instanceof RuntimeException) {throw new UncheckedExecutionException(cause);}throw var13;} finally {this.postReadCleanup();}return var15;}

getLiveValue

 V getLiveValue(ReferenceEntry<K, V> entry, long now) {//被GC回收if (entry.getKey() == null) {this.tryDrainReferenceQueues();return null;} else {V value = entry.getValueReference().get();//被GC回收if (value == null) {this.tryDrainReferenceQueues();return null;//如果没有设置expiresAfterAccess或者expiresAfterWrite,直接返回false,缓存有效//如果设置了,但是还在过期时间内,返回false  缓存有效//如果设置了,超过了过期时间,返回true   缓存无效} else if (this.map.isExpired(entry, now)) {this.tryExpireEntries(now);return null;} else {//缓存有效,直接返回valuereturn value;}}}

isExpired

 boolean isExpired(ReferenceEntry<K, V> entry, long now) {Preconditions.checkNotNull(entry);if (this.expiresAfterAccess() && now - entry.getAccessTime() >= this.expireAfterAccessNanos) {return true;} else {return this.expiresAfterWrite() && now - entry.getWriteTime() >= this.expireAfterWriteNanos;}}

scheduleRefresh

//判断是否设置了刷新,并且访问时间超过了刷新的时间,表明该进行刷新,但同时需要判断该值是否正在刷新
//如果该值正在刷新,则直接返回旧值,判断该值是否正在刷新的主要依据就是isloading,当其他线程在刷新该值的时候,事实上把entry中的ValueReference重新设置成了,实现ValueReference的LoadingValueReference,如果说其他线程没有在进行loading,说明还是之前的ValueReference,ValueReference.isloading()默认返会false, 如果是有线程在进行刷新,就是将原ValueReference内容替换为LoadingValueReference,LoadingValueReference.isloading()默认返回true,
//调用scheduleRefresh,说明expire相关的设置还在有效期内,会根据是否设置刷新的操作,来判断需不需要超时刷新,
//分两种情况,
//1.多线程的情况下,需要进行refresh操作,一个线程进行loading操作,调用refresh后,首先先进行将valueRefrence替换为loadingvalueReference,然后再真正的loading操作,也就是调用loadAsync,V scheduleRefresh(ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader{if (this.map.refreshes() && now - entry.getWriteTime() > this.map.refreshNanos && //这里的isloading为true,表明其他线程正在进行刷新的操作,其他线程在刷新,当前线程进行判断后,直接返回原值!entry.getValueReference().isLoading()) {V newValue = this.refresh(key, hash, loader, true);if (newValue != null) {return newValue;}}return oldValue;}

refresh
refresh 操作是针对了设置refresh超时时间的,并且进行调用loadAsync,对于refresh来说进入到loadAsync方法中进行判断,最终调用的loader.reload方法进行刷新缓存,取决于reload方法是同步还是异步的操作。下面代码详细说明

//进行refresh操作,多线程中,只有一个线程可以进行refresh的操作。
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {//插入LoadingValueReference去替换之前的ValueReference,目的就是为了能够体现该值正在刷新中,相当于一个挂牌的标识,LocalCache.LoadingValueReference<K, V> loadingValueReference = this.insertLoadingValueReference(key, hash, checkTime);if (loadingValueReference == null) {return null;} else {//真正的刷新操作在这里进行执行,//如果是异步刷新,this.loadAsync会直接返回,而不用等到缓存值刷新才返回,所以接下来的result.isDone()可能为false,因为//this.loadAsync返回的是reload异步调用中国的calllable对象,所以下面的isdone是判断异步任务是否执行完成,因为如果热reload//操作是异步的,则直接返回一个包装callable的ListenableFuture,在下面isdone判断执行的时候,异步任务可能还没来得及执行,或者//是还没有执行完成,那么此时加载缓存的线程此时执行到这里就返回null,上一层判断为null之后就返回旧值,但也可能存在异步执行操作结果完成,那么此时 return Uninterruptibles.getUninterruptibly(result)就是新值,//所以说,对于异步刷新来说,刷新的线程可能获得旧值,也可能获取新值//reload如果是同步操作,那么此时调用this.loadAsync->loadFuture->reload 则是执行完成才会返回,而不是立即返回,所以同步刷新的时候,判断result.isDone()一定是true,所以,一定返回新值,//所以说对于同步刷新来说,刷新的线程一定获取的是新值,ListenableFuture<V> result = this.loadAsync(key, hash, loadingValueReference, loader);if (result.isDone()) {try {return Uninterruptibles.getUninterruptibly(result);} catch (Throwable var8) {}}return null;}}

loadAsync:异步加载的过程

ListenableFuture<V> loadAsync(final K key, final int hash, final LocalCache.LoadingValueReference<K, V> loadingValueReference, CacheLoader<? super K, V> loader) {final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);//当加载完成的时候,执行下边的操作loadingFuture.addListener(new Runnable() {public void run() {try {Segment.this.getAndRecordStats(key, hash, loadingValueReference, loadingFuture);} catch (Throwable var2) {LocalCache.logger.log(Level.WARNING, "Exception thrown during refresh", var2);loadingValueReference.setException(var2);}}}, MoreExecutors.directExecutor());return loadingFuture;}

loadSync:同步加载的过程

V loadSync(K key, int hash, LocalCache.LoadingValueReference<K, V> loadingValueReference, CacheLoader<? super K, V> loader) throws ExecutionException {ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);return this.getAndRecordStats(key, hash, loadingValueReference, loadingFuture);}

loadFuture
这个方法在对于expire设置过期和refresh设置过期的情况都会调用到这个方法,expire设置过期的时候会调用loadSync方法,refresh设置过期的话会调用loadAsync方法,这两个方法都会调用loadFuture这个方法,因为这个loadFuture方法是LoadingValueReference静态内部类中的方法,所以下面的this表示就是LoadingValueReference,而这个LoadingValueReference是在loadSync方法和loadAsync进行传入的,
LoadingValueReference的不同之处就在于,在调用loadSync和loadAsync之前,LoadingValueReference是对expire和refresh的设置变化的,如果是expire设置过期,在真正加载缓存之前,需要进行一个挂牌的操作,也就是表示该值证在加载吗,其他线程不能加载,对于expire来说,是直接new 一个新的了loadingValueReference,进行替换valueRefrence,而对于refresh设置过期的话,会将之前的valueReference的内容包装成一个LoadingvalueReference,所以对于expire来说,新的loadingvalueReference
中的值为null,对于refresh来说新的loadingValueReferece中的value为旧值。

接下来说下面具体的方法

 public ListenableFuture<V> loadFuture(K key, CacheLoader<? super K, V> loader) {Object result;try {this.stopwatch.start();V previousValue = this.oldValue.get();//这里的this就是在loadSync和loadAsync方法传入的loadingvalueReference//如果说this.oldValue.get()为null,那就说明是设置了expire相关,并且超过了过期时间需要加载的情况,不为null表明是设置了refresh相关,并且缓存获取,需要加载,//所以可以看出,对于expire来说执行if判断中的语句,自然也就实行load,这也就是之前说的对于expire来说,缓存是进行同步加载,多线程访问缓存,发现缓存过期的时候,只有一个线程去同步加载缓存,其他线程进行等待,所以说对于设置饿了expire来说,当缓存过期的时候,同步加载,并且每个线程都拿到的是最新的值,//那么对于refresh来说,就执行else中的语句,调用reload,这里就需要看传入的loader覆盖的reload方法是同步还是异步,如果是//异步执行的话,就是异步,同步的话就是同步加载。if (previousValue == null) {result = loader.load(key);return (ListenableFuture)(this.set(result) ? this.futureValue : Futures.immediateFuture(result));} else {//执行覆盖的reload方法,如果reload是异步操作就是异步加载,如果reload是同步操作,就是同步加载//这里如果是异步加载,就可以直接进行返回,如果是同步加载,加载完才进行返回//ListenableFuture<V> newValue = loader.reload(key, previousValue);return newValue == null ? Futures.immediateFuture((Object)null) : Futures.transform(newValue, new com.google.common.base.Function<V, V>() {public V apply(V newValue) {LoadingValueReference.this.set(newValue);return newValue;}}, MoreExecutors.directExecutor());}} catch (Throwable var5) {result = this.setException(var5) ? this.futureValue : this.fullyFailedFuture(var5);if (var5 instanceof InterruptedException) {Thread.currentThread().interrupt();}return (ListenableFuture)result;}}
V waitForLoadingValue(ReferenceEntry<K, V> e, K key, LocalCache.ValueReference<K, V> valueReference) throws ExecutionException {if (!valueReference.isLoading()) {throw new AssertionError();} else {Preconditions.checkState(!Thread.holdsLock(e), "Recursive load of: %s", key);Object var7;try {//而同步等待是在判断值正在被loading的时候进行同步等待,代码中同步等待获取值valueReference.waitforvalue,由于判断值证在更新,所以才同步等待,此时的valueReference已经变为loadingValueReference,所以在waitForValue的时候获取到的是loadingValueReference中的成员future的值,而这个值是在被load或者reload的完成之后进行set到future的V value = valueReference.waitForValue();if (value == null) {throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");}long now = this.map.ticker.read();this.recordRead(e, now);var7 = value;} finally {this.statsCounter.recordMisses(1);}return var7;}}
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()) {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 {//同步加载过程,load是事实上调用loadFuturesynchronized(e) {var9 = this.loadSync(key, hash, loadingValueReference, loader);}} finally {this.statsCounter.recordMisses(1);}return var9;} else {return this.waitForLoadingValue(e, key, valueReference);}}

本地缓存-loadingCache相关推荐

  1. 万字详解本地缓存之王 Caffeine

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 来自:r6d.cn/UXR4 概要 Caffeine[1] ...

  2. guava_使用Google Guava Cache进行本地缓存

    guava 很多时候,我们将不得不从数据库或另一个Web服务获取数据或从文件系统加载数据. 在涉及网络呼叫的情况下,将存在固有的网络延迟,网络带宽限制. 解决此问题的方法之一是在应用程序本地拥有一个缓 ...

  3. 干掉 GuavaCache:Caffeine 才是本地缓存的王

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 话说,中间件的选择上,Spring(SpringBoot ...

  4. Caffeine Cache~高性能 Java 本地缓存之王

    前面刚说到Guava Cache,他的优点是封装了get,put操作:提供线程安全的缓存操作:提供过期策略:提供回收策略:缓存监控.当缓存的数据超过最大值时,使用LRU算法替换.这一篇我们将要谈到一个 ...

  5. 使用Google Guava Cache进行本地缓存

    很多时候,我们将不得不从数据库或另一个Web服务获取数据或从文件系统加载数据. 在涉及网络呼叫的情况下,将存在固有的网络等待时间,网络带宽限制. 解决此问题的方法之一是在应用程序本地拥有一个缓存. 如 ...

  6. 本地缓存之Guava简单使用

    文章目录 使用场景 Guava Cache 的优势 Guava Cache使用 CacheLoader Callable 删除 主动删除 过期删除 基于容量删除 引用删除 高级用法 并发设置 更新锁定 ...

  7. ehcache缓存原理_干掉GuavaCache:Caffeine才是本地缓存的王

    话说,中间件的选择上,Spring(SpringBoot)一直是业界的风向标.比如Spring一直使用「Jackson」,而没有使用Gson和fastjson.SpringBoot2.0默认数据库连接 ...

  8. 本地缓存与分布式缓存

    更多内容,前往 IT-BLOG 一般而言,现在互联网应用(网站或App)的整体流程,可以概括如图所示,用户请求从界面(浏览器或App界面)到网络转发.应用服务再到存储(数据库或文件系统),然后返回到界 ...

  9. 看了阿里大佬用的本地缓存,那叫一个优雅!

    本文经授权转载自微信公众号:阿里开发者,作者:杨贤(临景) Java缓存技术可分为远端缓存和本地缓存,远端缓存常用的方案有著名的redis和memcache,而本地缓存的代表技术主要有HashMap, ...

最新文章

  1. mongodb地理位置索引实现原理
  2. C++中类的静态成员
  3. MySQL LIMIT 如何改写成Oracle limit
  4. orale客户端与数据库连接
  5. nagios监控mysql主机,nginx,cpu,网卡流量
  6. 在python中单线程,多线程,多进程对CPU的利用率实测以及GIL原理分析
  7. 链接数据库 并且进行查询操作
  8. Anroid 开发so文件找不到问题-例高德地图SDK提示com.autonavi.amap.mapcore.MapCore.nativeNewInstance问题
  9. pb11.5调用系统打印机
  10. BoundsChecker安装下载及使用教程攻略
  11. 如何精细化APP运营
  12. 解决台式机前耳机插孔没有声音
  13. cdr文字内容显示不出来_cdr中字体预览不显示 字体安装后cdr不显示
  14. [软件测试]软件测试的原则及软件质量
  15. 用wvdial和ppp轻松上网
  16. css中white-space 属性:“pre”, “pre-line”, “pre-wrap” “nowrap”的区别
  17. 程序员如何接私活、外包的秘技
  18. 【若依(ruoyi)】template might not exist or might not be accessible by any of the configured Template Res
  19. R 数据分析方法(梅长林)exercise1-3
  20. css less 文件:global的写法

热门文章

  1. 磁性材料的特性、工艺及检测知识汇总
  2. 某拍网登录接口参数加密流程
  3. Angel_天使PE优盘启动工具网络纯净版v2023.01.12
  4. ”每股净资产”与“每股未分配利润”
  5. 【学习笔记】Tensorflow-ENet代码学习(二)
  6. 测试电脑GPU性能代码
  7. 2012禁用ip隧道 win_windows server 2008 R2 禁用ipv6和隧道适配器
  8. QT5.12+opencv4.0.1 Cielab空间 像素颜色信息
  9. 数据结构——C语言实现快速排序算法
  10. 多群转播,有什么好用的微信社群管理软件推荐?