本地缓存-loadingCache

2024年C C++最全BFS经典例题合集_bfs例题,2024年最新字节跳动+京东+美团+腾讯面试总结 【代码】2024年C C++最全BFS经典例题合集_bfs例题,2024年最新字节跳动+京东+美团+腾讯面试总结。 阅读详情

本地缓存

缓存大概是一个不能再熟悉的话题,今天的主要内容是分享一下我们公司使用比较多的本地缓存 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 {
                //缓存有效,直接返回value
  
                    return 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是事实上调用loadFuture
                    synchronized(e) {
                        var9 = this.loadSync(key, hash, loadingValueReference, loader);
                    }
                } finally {
                    this.statsCounter.recordMisses(1);
                }

                return var9;
            } else {
                return this.waitForLoadingValue(e, key, valueReference);
            }
        }
高并发技巧-redis和本地缓存使用技巧 这里redis的使用就不再多说了,相信很多人对api的使用比我还熟悉。 阅读详情

相关推荐

Guava Cache 原理分析与最佳实践

本讲主要针对本地 Cache 的老大哥 Guava Cache 进行介绍和分析。

Java Punk 6977

本地缓存LoadingCache使用【详解】

LoadingCache 是 Guava 库提供的一个本地缓存实现,它具有自动加载和缓存过期等特性。使用 LoadingCache 可以快速地缓存数据,避免重复计算或远程调用,提高应用程序的性能和响应速度。

ᶻᶻᶻ 枕星河的博客 6593

创建可视化机房

(3)窗户位置创建好以后需要对窗户的墙面镂空位置进行修补,具体操作为,选中镂空位置的顶点(这里必须是选择一个平面的顶点,不要跨平面选择),实际操作如下图:选择点以后,鼠标右键,选择【从顶点创建边/面】,或者按F键,随后对其他镂空位置进行相同操作;(2)将【物体模式】修改为【编辑模式】,选中-x对应面的所有顶点,移动顶点位置,其他放小调整按照同理,找到选中对应面的所有顶点,移动位置即可;调整动画事件轴上的线,选中关键帧的时间,调整模型的位置或者旋转角度,具体根据插入关键帧菜单选择的项进行模型的相关调整。

SmileBu丿㦌的博客 2067

LoadingCache缓存使用(LoadingCache)

LoadingCache缓存使用(LoadingCache) 一 、引入依赖 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>24.1-jre</version> </dependency> 二、代码案例 package com.example.demo; import

codesweetpotato的博客 7984

Dify工作流自动化:如何用Trae AI助手10倍提升你的开发效率(含避坑指南)

本文详细介绍了如何利用Trae AI助手自动化生成Dify工作流,实现开发效率的10倍提升。通过构建DSL示例库、精准需求描述和四步实战法,开发者可将重复的配置工作交给AI,快速生成可运行的工作流,并提供了关键的避坑指南。

weixin_29062865的博客 370

Qlib+backtrader:2014.1.1-2023.9.20最新回测结果,可以实盘吗?

今年以来,在研究了qlib和backtrader的基础上,把二者结合起来进行了一个策略研究。简单说就是用qlib在200只股票的股票池中进行滚动训练与预测(walk forward),总体数据范围是2005到2023年,以20日间隔滚动训练和预测,最终得到2014.1.1-2023.9.20共10年测试期的收益率预测结果。将预测结果代入backtrader进行回测,得到如下回测结果。

qtbgo的博客 1783

Google-LoadingCache本地缓存

最新在项目中发现一个LoadingCache类。是Google提供的一种本地缓存。挺好使的。分享一下。 Maven: <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> ...

致终将逝去的编程青春 1486

离散数学期末复习笔记【精华版】

文章目录前言一、????理逻辑1. 命题逻辑2. 等值演算3. 主合取范式、主析取范式4. 【题】推理证明5. 辖域、自由变元、约束变元二、????合1. 环和、环积,幂集,笛卡尔乘积三、 ????????关系1. 自反、对称、传递2&3. 合成运算,逆关系、闭包4. 哈斯图、上下界、偏序5. 等价关系、划分四、????1. 幺元(么元、单位元)、零元、逆元,半群、群、阿尔贝群、独异点、循环独异点2. 【题】由表判断3. 【题】证明五、????论1. 简单图、平凡图、多重图、子图、补图、出度、入度、

林大夕可的博客 17万+

Java本地缓存-LoadingCache

一、使用场景 缓存的作用不言而喻就是提高查询效率,本地缓存就是利用空间换时间的一种获取资源的方式,像我们自己维护一个ConcurrentMap,就实现了最简单的本地缓存,但是需要我们自己去做一些缓存的策略,例如缓存过期、缓存清除等该怎么做。那么相应的就会出现一些本地缓存框架,本文主要记录LoadingCache的使用。 想我之前的一家公司会在项目启动的时候加载一些配置信息和枚举值到Map中,然后会开放接口用于新增、刷新缓存。 二、基本使用 依赖: <!-- https://mvnrepos

minion_banana的博客 1778

增量学习入门:概念、代码示例与应用场景

在当今快速变化的数据环境中,传统的机器学习模型往往需要重新训练以适应新的数据。然而,这种方法不仅耗时耗力,还可能导致模型性能的不稳定。增量学习(Incremental Learning)作为一种新兴的学习范式,能够在新数据到来时动态更新模型,而无需重新训练整个模型。本文将详细介绍增量学习的基本概念、代码示例、应用场景以及需要注意的事项。增量学习作为一种高效的机器学习方法,能够在新数据到来时动态更新模型,具有广泛的应用前景。通过本文的介绍,相信你对增量学习有了初步的了解。

加入“Super Entity”,与全能开发团队共探AI智能体与数字人项目,开启前沿技术之旅。 1152

Python 爬虫实战:攻克小红书种草笔记,抓取美妆好物推荐数据

通过本次 Python 爬虫实战,我们成功地从小红书爬取了美妆种草笔记的数据,并进行了存储、处理、分析和可视化展示。在这个过程中,我们不仅掌握了爬虫技术的基本应用,还学会了如何对动态加载的数据进行处理和分析。然而,本次爬虫任务也存在一些局限性,例如仅爬取了部分笔记的数据,且未对笔记中的图片和视频内容进行分析。在未来的工作中,我们可以进一步优化爬虫程序,爬取更多笔记的数据,并深入分析笔记中的多媒体内容、用户行为模式等,为美妆品牌和消费者提供更有价值的数据支持。

u014481728的博客 3269

Guava Cache本地缓存(LoadingCache)

在开发应用中,避免不了会使用到缓存,分布式缓存一般会用到redis、Memcache等常用,本地缓存像ehcache这种用的也是很多 今天介绍下google guava框架插件提供的LoadingCache本地缓存,LoadingCache和ConcurrentMap差不多,都是线程安全的,只不过比ConcurrentMap多了一些其他的功能,如过期策略等 优点 1、线程安全的缓存,与Co...

程序员 3065

CMOS Sensor的调试经验分享

我这里要介绍的就是CMOS摄像头的一些调试经验。 首先,要认识CMOS摄像头的结构。我们通常拿到的是集成封装好的模组,一般由三个部分组成:镜头、感应器和图像信号处理器构成。一般情况下,集成好的模组我们只看到外面的镜头、接口和封装壳,这种一般是固定焦距的。有些厂商只提供芯片,需要自己安装镜头,镜头要选择合适大小的镜头,如果没有夜视要求的话,最好选择带有红外滤光的镜头,因为一般的sensor都...

camerall 3763

【JAVA】本地缓存(LoadingCache)

一、参考资料 java 缓存架构剖析--本地缓存(LoadingCache)

后端研发工程师Marion的博客 429

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

本地缓存 - LoadingCache

面试经常会被问到如何解决缓存击穿问题,今天就来带你弄懂他!平时业务中也会经常使用到本地缓存,公司里使用比较多的本地缓存 loadingcache,其背后的架构就是Guava cache,Guava Cache 是一个全内存的本地缓存实现,它提供了线程安全的实现机制。 整体上来说Guava Cache 是本地缓存的不二之选。

a821230的博客 2419

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

google本地缓存LoadingCache

google本地缓存LoadingCache 1.需要引入相关pom依赖(项目源码参考:https://github.com/licunzhi/dream_on_sakura_rain/tree/master/loadingcache_demo) com.google.guava guava 24.1-jre 2.在项目启动的时候注册组件Bean(使用注解@Confi...

weixin_43837889的博客 569

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Java基于LoadingCache实现本地缓存

Java基于LoadingCache实现本地缓存 一、 添加maven依赖 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.1-jre</version> </dependency> 二、CacheBuilder方法说明 1️⃣LoadingCa.

qq_35139965的博客 949

google本地缓存LoadingCache使用

在项目中语使用本地缓存记录一下,具体代码如下: @Service public class StrategyLocal { private static final Logger LOG = LoggerFactory.getLogger(StrategyLocal.class); @Autowired private RouteInfoDAO routeInfoDAO; private

bit-cafe 6282

Guava Cache、LoadingCache本地缓存的使用

Guava Cache、LoadingCache本地缓存的使用

zy_javapythonc的博客 1087

基于Guava LoadingCache实现的本地缓存工具

通常来说,Guava Cache适用于: 愿意消耗一些内存空间来提升速度。 预料到某些键会被查询一次以上。 缓存中存放的数据总量不会超出内存容量。 Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。在重启服务器时缓存的数据会全部丢失。如果这不符合你的需求,请尝试Redis或Memcached这类工具。 缓存工具的具体实现如下: import c

Milton1993的专栏 5655

【干货分享】Guava LoadingCache 使用指南:打造高性能本地缓存

本文介绍了基于Guava LoadingCache实现的缓存管理器,其核心特性包括自动加载、灵活的缓存配置和批量操作。通过CacheBuilder可配置缓存容量、过期时间及统计功能,采用懒加载机制仅在首次访问时从数据源加载数据,支持基于时间和容量的自动回收策略。该方案适用于高频访问、计算成本高且对数据一致性要求不严格的场景,如用户信息缓存,能有效提升系统性能。文章还详细解析了其工作原理和关键特性,包括线程安全、统计功能和异常处理机制。

qq_33651286的博客 619

java本地缓存-LoadingCache使用

缓存的作用是用来保存一些程序运行中需要经常用的数据。这些数据如果存在数据库,频繁的访问数据库会增加数据库的负载。所以要将这些数据放在本地缓存中,并定时更新。 LoadingCache的使用过程中主要涉及两个类LoadingCache和CacheBuilder。 CacheBuilder使用来定义一些更新删除策略,最后生成LoadingCache对象。 CacheBuilder的主要方法: refreshAfterWrite(long, TimeUnit):缓存增加自动定时刷新功能。在获取数据的时候如

毅香雪海的博客 7863

loadingcache java_Java基于LoadingCache实现本地缓存

一、 添加maven依赖com.google.guavaguava27.1-jre二、CacheBuilder方法说明1️⃣LoadingCache build(CacheLoader loader)2️⃣CacheBuilder.maximumSize(long size)配置缓存数量上限,快达到上限或达到上限,处理了时间最长没被访问过的对象或者根据配置的被释放的对象3️⃣expireAfter...

weixin_34000790的博客 521
上一篇: BIO输给NIO了吗 —————— 开开开山怪
开开开山怪
博客等级 码龄8年 41粉丝 30原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值