城市直播房产教育博客汽车
投稿投诉
汽车报价
买车新车
博客专栏
专题精品
教育留学
高考读书
房产家居
彩票视频
直播黑猫
投资微博
城市上海
政务旅游

探寻JVM级别的本地缓存框架GuavaCache实现细节与核

6月26日 逆落雪投稿
  大家好,又见面了。
  本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面。如果感兴趣,欢迎关注以获取后续更新。
  通过《重新认识下JVM级别的本地缓存框架GuavaCache优秀从何而来》一文,我们知道了GuavaCache作为JVM级别的本地缓存组件的诸多暖心特性,也一步步地学习了在项目中集成并使用GuavaCache进行缓存相关操作。GuavaCache作为一款优秀的本地缓存组件,其内部很多实现机制与设计策略,同样值得开发人员深入的掌握与借鉴。
  作为系列专栏,本篇文章我们将进一步探讨下GuavaCache实现层面的一些逻辑与设计策略,让我们可以对GuavaCache整体有个更加明朗的认识。
  数据回源与回填策略
  前面我们介绍过,GuavaCache提供的是一种穿透型缓存模式,当缓存中没有获取到对应记录的时候,支持自动去源端获取数据并回填到缓存中。这里回源获取数据的策略有两种,即CacheLoader方式与Callable方式,两种方式适用于不同的场景,实际使用中可以按需选择。
  下面一起看下这两种方式。CacheLoader
  CacheLoader适用于数据加载方式比较固定且统一的场景,在缓存容器创建的时候就需要指定此具体的加载逻辑。常见的使用方式如下:publicLoadingCacheString,UsercreateUserCache(){returnCacheBuilder。newBuilder()。build(newCacheLoaderString,User(){OverridepublicUserload(Stringkey)throwsException{System。out。println(key用户缓存不存在,尝试CacheLoader回源查找并回填。。。);returnuserDao。getUser(key);}});}
  上述代码中,在使用CacheBuilder创建缓存容器的时候,如果在build()方法中传入一个CacheLoader实现类的方式,则最终创建出来的是一个LoadingCache具体类型的Cache容器:
  默认情况下,我们需要继承CacheLoader类并实现其load抽象方法即可。
  当然,CacheLoader类中还有一些其它的方法,我们也可以选择性的进行覆写来实现自己的自定义诉求。比如我们需要设定refreshAfterWrite来支持定时刷新的时候,就推荐覆写reload方法,提供一个异步数据加载能力,避免数据刷新操作对业务请求造成阻塞。
  另外,有一点需要注意下,如果创建缓存的时候使用refreshAfterWrite指定了需要定时更新缓存数据内容,则必须在创建的时候指定CacheLoader实例,否则执行的时候会报错。因为在执行refresh操作的时候,必须调用CacheLoader对象的reload方法去执行数据的回源操作。
  Callable
  与CacheLoader不同,Callable的方式在每次数据获取请求中进行指定,可以在不同的调用场景中,分别指定并使用不同的数据获取策略,更加的灵活。publicstaticvoidmain(String〔〕args){try{GuavaCacheServicecacheServicenewGuavaCacheService();CacheString,UsercachecacheService。createCache();StringuserId123;获取userId,获取不到的时候执行Callable进行回源Userusercache。get(userId,()cacheService。queryUserFromDb(userId));System。out。println(get对应用户信息:user);}catch(Exceptione){e。printStackTrace();}}
  通过提供Callable实现函数并作为参数传递的方式,可以根据业务的需要,在不同业务调用场景下,指定使用不同的Callable回源策略。
  不回源查询
  前面介绍了基于CacheLoader方式自动回源,或者基于Callable方式显式回源的两种策略。但是实际使用缓存的时候,并非是缓存中获取不到数据时就一定需要去执行回源操作。
  比如下面这个场景:
  用户论坛中维护了个黑名单列表,每次用户登录的时候,需要先判断下是否在黑名单中,如果在则禁止登录。
  因为论坛中黑名单用户占整体用户量的比重是极少的,也就是几乎绝大部分登录的时候去查询缓存都是无法命中黑名单缓存的。这种时候如果每次查询缓存中没有的情况下都去执行回源操作,那几乎等同于每次请求都需要去访问一次DB了,这显然是不合理的。
  所以,为了支持这种场景的访问,Guavacache也提供了一种不会触发回源操作的访问方式。如下:
  接口
  功能说明
  getIfPresent
  从内存中查询,如果存在则返回对应值,不存在则返回null
  getAllPresent
  批量从内存中查询,如果存在则返回存在的键值对,不存在的key则不出现在结果集里
  上述两种接口,执行的时候仅会从当前内存中已有的缓存数据里面进行查询,不会触发回源的操作。publicstaticvoidmain(String〔〕args){try{GuavaCacheServicecacheServicenewGuavaCacheService();CacheString,UsercachecacheService。createCache();cache。put(123,newUser(123,123));cache。put(124,newUser(124,124));System。out。println(cache。getIfPresent(125));ImmutableMapString,UserallPresentUserscache。getAllPresent(Stream。of(123,124,125)。collect(Collectors。toList()));System。out。println(allPresentUsers);}catch(Exceptione){e。printStackTrace();}}
  执行后,输入结果如下:null{123User(userName123,userId123),124User(userName124,userId124)}GuavaCache的数据清理与加载刷新机制
  在前面的CacheBuilder类中有提供了几种expire与refresh的方法,即expireAfterAccess、expireAfterWrite以及refreshAfterWrite。
  这里我们对几个方法进行一些探讨。数据过期
  对于数据有过期时效诉求的场景,我们可以通过几种方式设定缓存的过期时间:expireAfterAccessexpireAfterWrite
  现在我们思考一个问题:数据过期之后,会立即被删除吗?在前面的文章中,我们自己构建本地缓存框架的时候,有介绍过缓存数据删除的几种机制:
  删除机制
  具体说明
  主动删除
  搞个定时线程不停地去扫描并清理所有已经过期的数据。
  惰性删除
  在数据访问的时候进行判断,如果过期则删除此数据。
  两者结合
  采用惰性删除为主,低频定时主动删除为兜底,兼顾处理性能与内存占用。
  在GuavaCache中,为了最大限度的保证并发性,采用的是惰性删除的策略,而没有设计独立清理线程。所以这里我们就可以回答前面的问题,也即过期的数据,并非是立即被删除的,而是在get等操作访问缓存记录时触发过期数据的删除操作。
  在get执行逻辑中进行数据过期清理以及重新回源加载的执行判断流程,可以简化为下图中的关键环节:
  在执行get请求的时候,会先判断下当前查询的数据是否过期,如果已经过期,则会触发对当前操作的Segment的过期数据清理操作。数据刷新
  除了上述的2个过期时间设定方法,GuavaCache还提供了个refreshAfterWrite方法,用于设定定时自动refresh操作。项目中可能会有这么个情况:
  为了提升性能,将最近访问系统的用户信息缓存起来,设定有效期30分钟。如果用户的角色出现变更,或者用户昵称、个性签名之类的发生更改,则需要最长等待30分钟缓存失效重新加载后才能够生效。
  这种情况下,我们就可以在设定了过期时间的基础上,再设定一个每隔1分钟重新refresh的逻辑。这样既可以保证数据在缓存中的留存时长,又可以尽可能地缩短缓存变更生效的时间。这种情况,便该refreshAfterWrite登场了。
  与expire清理逻辑相同,refresh操作依旧是采用一种被动触发的方式来实现。当get操作执行的时候会判断下如果创建时间已经超过了设定的刷新间隔,则会重新去执行一次数据的加载逻辑(前提是数据并没有过期)。
  鉴于缓存读多写少的特点,GuavaCache在数据refresh操作执行的时候,采用了一种非阻塞式的加载逻辑,尽可能的保证并发场景下对读取线程的性能影响。
  看下面的代码,模拟了两个并发请求同时请求一个需要刷新的数据的场景:publicstaticvoidmain(String〔〕args){try{LoadingCacheString,UsercacheCacheBuilder。newBuilder()。refreshAfterWrite(1L,TimeUnit。SECONDS)。build(newMyCacheLoader());cache。put(123,newUser(123,ertyu));Thread。sleep(1100L);Runnabletask(){try{System。out。println(Thread。currentThread()。getId()线程开始执行查询操作);Userusercache。get(123);System。out。println(Thread。currentThread()。getId()线程查询结果:user);}catch(Exceptione){e。printStackTrace();}};CompletableFuture。allOf(CompletableFuture。runAsync(task),CompletableFuture。runAsync(task))。thenRunAsync(task)。join();}catch(Exceptione){e。printStackTrace();}}
  执行后,结果如下:14线程开始执行查询操作13线程开始执行查询操作13线程查询结果:User(userNameertyu,userId123)14线程执行CacheLoader。reload(),oldValueUser(userNameertyu,userId123)14线程执行CacheLoader。load()。。。14线程执行CacheLoader。load()结束。。。14线程查询结果:User(userName97qx6,userId123)15线程开始执行查询操作15线程查询结果:User(userName97qx6,userId123)
  从执行结果可以看出,两个并发同时请求的线程只有1个执行了load数据操作,且两个线程所获取到的结果是不一样的。具体而言,可以概括为如下几点:同一时刻仅允许一个线程执行数据重新加载操作,并阻塞等待重新加载完成之后该线程的查询请求才会返回对应的新值作为结果。当一个线程正在阻塞执行refresh数据刷新操作的时候,其它线程此时来执行get请求的时候,会判断下数据需要refresh操作,但是因为没有获取到refresh执行锁,这些其它线程的请求不会被阻塞等待refresh完成,而是立刻返回当前refresh前的旧值。当执行refresh的线程操作完成后,此时另一个线程再去执行get请求的时候,会判断无需refresh,直接返回当前内存中的当前值即可。
  上述的过程,按照时间轴的维度来看,可以囊括成如下的执行过程:
  数据expire与refresh关系
  expire与refresh在某些实现逻辑上有一定的相似度,很多的文章中介绍的时候甚至很直白的说refresh比expire更好,因为其不会阻塞业务端的请求。个人认为这种看法有点片面,从单纯的字面含义上也可以看出这两种机制不是互斥的、而是一种相互补充的存在,并不存在谁比谁更好这一说,关键要看具体是应用场景。
  expire操作就是采用的一种严苛的更新锁定机制,当一个线程执行数据重新加载的时候,其余的线程则阻塞等待。refresh操作执行过程中不会阻塞其余线程的get查询操作,会直接返回旧值。这样的设计各有利弊:
  操作
  优势
  弊端
  expire
  有效防止缓存击穿问题,且阻塞等待的方式可以保证业务层面获取到的缓存数据的强一致性。
  高并发场景下,如果回源的耗时较长,会导致多个读线程被阻塞等待,影响整体的并发效率。
  refresh
  可以最大限度的保证查询操作的执行效率,避免过多的线程被阻塞等待。
  多个线程并发请求同一个key对应的缓存值拿到的结果可能不一致,在对于一致性要求特别严苛的业务场景下可能会引发问题。
  GuavaCache中的expire与fefresh两种机制,给人的另一个困惑点在于:两者都是被动触发的数据加载逻辑,不管是expire还是refresh,只要超过指定的时间间隔,其实都是依旧存在与内存中,等有新的请求查询的时候,都会执行自动的最新数据加载操作。那这两个实际使用而言,仅仅只需要依据是否需要阻塞加载这个维度来抉择?
  并非如此。
  expire存在的意义更多的是一种数据生命周期终结的意味,超过了expire有效期的数据,虽然依旧会存在于内存中,但是在一些触发了cleanUp操作的情况下,是会被释放掉以减少内存占用的。而refresh则仅仅只是执行数据更新,框架无法判断是否需要从内存释放掉,会始终占据内存。
  所以在具体使用时,需要根据场景综合判断:数据需要永久存储,且不会变更,这种情况下expire和refresh都并不需要设定数据极少变更,或者对变更的感知诉求不强,且并发请求同一个key的竞争压力不大,直接使用expire即可数据无需过期,但是可能会被修改,需要及时感知并更新缓存数据,直接使用refresh数据需要过期(避免不再使用的数据始终留在内存中)、也需要在有效期内尽可能保证数据的更新一致性,则采用expire与refresh两者结合。
  对于expire与refresh结合使用的场景,两者的时间间隔设置,需要注意:
  expire时间设定要大于refresh时间,否则的话refresh将永远没有机会执行GuavaCache并发能力支持采用分段锁降低锁争夺
  前面我们提过GuavaCache支持多线程环境下的安全访问。我们知道锁的粒度越大,多线程请求的时候对锁的竞争压力越大,对性能的影响越大。而如果将锁的粒度拆分小一些,这样同时请求到同一把锁的概率就会降低,这样线程间争夺锁的竞争压力就会降低。
  GuavaCache中采用的也就是这种分段锁策略来降低锁的粒度,可以在创建缓存容器的时候使用concurrencyLevel来指定允许的最大并发线程数,使得线程安全的前提下尽可能的减少锁争夺。而concurrencyLevel值与分段Segment的数量之间也存在一定的关系,这个关系相对来说会比较复杂、且受是否限制总容量等因素的影响,源码中这部分的计算逻辑可以看下:intsegmentShift0;intsegmentCount1;while(segmentCountconcurrencyLevel(!evictsBySize()segmentCount20maxWeight)){segmentSsegmentCount1;}
  根据上述的控制逻辑代码,可以将segmentCount的取值约束概括为下面几点:segmentCount是2的整数倍segmentCount最大可能为(concurrencyLevel1)2如果有按照权重设置容量,则segmentCount不得超过总权重值的120
  从源码中可以比较清晰的看出这一点,GuavaCache在put写操作的时候,会首先计算出key对应的hash值,然后根据hash值来确定数据应该写入到哪个Segment中,进而对该Segment加锁执行写入操作。OverridepublicVput(Kkey,Vvalue){。。。省略部分逻辑inthashhash(key);returnsegmentFor(hash)。put(key,hash,value,false);}NullableVput(Kkey,inthash,Vvalue,booleanonlyIfAbsent){lock();try{。。。省略具体逻辑}finally{unlock();postWriteCleanup();}}
  根据上述源码也可以得出一个结论,concurrencyLevel只是一个理想状态下的最大同时并发数,也取决于同一时间的操作请求是否会平均的分散在各个不同的Segment中。极端情况下,如果多个线程操作的目标对象都在同一个分片中时,那么只有1个线程可以持锁执行,其余线程都会阻塞等待。
  实际使用中,比较推荐的是将concurrencyLevel设置为CPU核数的2倍,以获得较优的并发性能。当然,concurrencyLevel也不是可以随意设置的,从其源码设置里面可以看出,允许的最大值为65536。staticfinalintMAXSEGMENTS116;65536LocalCache(CacheB?superK,?superVbuilder,NullableCacheL?superK,Vloader){concurrencyLevelMath。min(builder。getConcurrencyLevel(),MAXSEGMENTS);。。。省略其余逻辑}佛系抢锁策略
  在put等写操作场景下,GuavaCache采用的是上述分段锁的策略,通过优化锁的粒度,来提升并发的性能。但是加锁毕竟还是对性能有一定的影响的,为了追求更加极致的性能表现,在get等读操作自身并没有发现加锁操作但是GuavaCache的get等处理逻辑也并非是纯粹的只读操作,它还兼具触发数据淘汰清理操作的删除逻辑,所以只在判断需要执行清理的时候才会尝试去佛系抢锁。
  那么它是如何减少抢锁的几率的呢?从源码中可以看出,并非是每次请求都会去触发cleanUp操作,而是会尝试积攒一定次数之后再去尝试清理:staticfinalintDRAINTHRESHOLD0x3F;voidpostReadCleanup(){if((readCount。incrementAndGet()DRAINTHRESHOLD)0){cleanUp();}}
  在高并发场景下,如果查询请求量巨大的情况下,即使按照上述的情况限制每次达到一定请求数量之后才去执行清理操作,依旧可能会出现多个get操作线程同时去抢锁执行清理操作的情况,这样岂不是依旧会阻塞这些读取请求的处理吗?
  继续看下源码:voidcleanUp(){longnowmap。ticker。read();runLockedCleanup(now);runUnlockedCleanup();}voidrunLockedCleanup(longnow){尝试请求锁,请求到就处理,请求不到就放弃if(tryLock()){try{。。。省略部分逻辑readCount。set(0);}finally{unlock();}}}
  可以看到源码中采用的是tryLock方法来尝试去抢锁,如果抢到锁就继续后续的操作,如果没抢到锁就不做任何清理操作,直接返回这也是为什么前面将其形容为佛系抢锁的缘由。这样的小细节中也可以看出Google码农们还是有点内功修为的。承前启后CaffeineCache
  技术的更新迭代始终没有停歇的时候,Guava工具包作为Google家族的优秀成员,在很多方面提供了非常优秀的能力支持。随着JAVA8的普及,Google也基于语言的新特性,对GuavaCache部分进行了重新实现,形成了后来的CaffeineCache,并在SpringBoot2。x中取代了GuavaCache。
  下一篇文章中,我们将一起再聊一聊令人上瘾的CaffeineCache。小结回顾
  好啦,关于GuavaCache中的典型实现机制与核心设计策略,就介绍到这里了。至此,我们完成了GuavaCache从简单会用到深度剖析的全过程,不知道小伙伴们是否对GuavaCache有了全新的认识了呢?关于GuavaCache,你是否有自己的一些想法与见解呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。
  补充说明1:本文属于《深入理解缓存原理与实战设计》系列专栏的内容之一。该专栏围绕缓存这个宏大命题进行展开阐述,全方位、系统性地深度剖析各种缓存实现策略与原理、以及缓存的各种用法、各种问题应对策略,并一起探讨下缓存设计的哲学。
  如果有兴趣,也欢迎关注此专栏。
  补充说明2:关于本文中涉及的演示代码的完整示例,我已经整理并提交到github中,如果您有需要,可以自取:https:github。comveezeanJavaBasicSkills
  我是悟道,聊技术、又不仅仅聊技术
  如果觉得有用,请点赞关注让我感受到您的支持。也可以关注下我的公众号【架构悟道】,获取更及时的更新。
  期待与你一起探讨,一起成长为更好的自己。
投诉 评论 转载

裸睡给了TA全新的生命体验听说,从前有一个人,叫果果,果果家徒四壁茹草饮土非常可怜,常常饥肠辘辘吃不饱饭,除了夜晚睡觉,白天饿得受不了的时候,果果也强迫自己睡觉保存体力积攒力量,一天天的屎都不拉一顿,生……探寻JVM级别的本地缓存框架GuavaCache实现细节与核大家好,又见面了。本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面。如果感兴趣,欢迎关注以获取后续更新。通过《……那一次我懂得了勇敢充满鲜花的世界到底在哪里?如果它真的存在,那么我一定回去。我想在那里最高的山峰矗立,不在乎它是不是悬崖峭壁。题记坐在车上,那一棵棵大树快速地后退,转眼间就在我的视线……怀孕初期出血原因有哪些怀孕初期出血是怎么回事呢?有的女性在怀孕初期有阴道出血的情况,也是我们俗称的见红,在临床上非常常见。那么,怀孕初期出血原因有哪些呢?引起妊娠早期出血的原因主要有以下几则:……大棚茄子杂草种类有哪些茄子别称落苏,在餐桌上很常见,也是为数不多的紫色蔬菜之一,具有去痛活血、清热消肿、解痛利尿的功能,对心血管病人来说是一种理想的菜肴,但是,茄子在种植的过程中会出现禾本科杂草,人……借世界杯难圆技术梦,雅迪舍本逐末难掩焦虑编辑于斌出品潮起网于见专栏近日,世界杯打响。在中国元素成为互联网热点的同时,雅迪也成为在世界杯亮相的中国元素。世界杯揭幕前一天,雅迪科技集团(简称雅迪)官宣:继续成……妈妈如何定时为宝宝调整睡姿你都知道吗新生儿不具备自己翻身的能力,所以在他们睡着时,妈妈要负责每隔四个小时,帮宝宝调整一下睡姿。那么,妈妈如何定时为宝宝调整睡姿?你都知道吗?就让本站的小编和您一起去了解一下吧!……惜别银杏叶的依依不舍头条创作挑战赛云衡微语哇塞!昨夜何来的风大地被银杏叶铺满厚厚的一层金黄,一些情不自禁有些手舞足蹈被惊艳感动Ta的唯美Ta的不舍这是一条开启幸福的分割线不……加盟老字号需要警惕什么随着特许加盟行业的快速发展,很多老字号希望通过这种加盟方式,获得更大的市场业绩。而很多老字号的追随者,也希望能够通过加盟老字号,创立自己的第一份个人事业。然而,面对市面上数量巨……麻将迷的梦想两麻将迷聊天,一谈到麻将,两人就兴奋不已。甲:我死后,什么都不用,烧副麻将给我就成,我就可以天天打麻将了!乙:最好是自动的!四进四出的,比较快,不用等很久,节省时间……神舟16号也准备就绪!2023年发射,是四选三,还是用外国航神舟15号载人飞船蓄势待发,时间定在11月29日晚,还剩最后几个小时,费俊龙、张陆、邓清明很快就能与空间站上的神舟14号航天员见面了,中国空间站将首次迎来6人同时在轨驻留的盛况……女性保健宫颈息肉的症状你清楚吗当妇科常规检查用阴道窥器暴露宫颈时,所见的息肉外形大小不等,形状不一,大致可分为两种:1、第一种子宫颈息肉根部多附着于宫颈外口,或在颈管内。一般体积较小,直径多在1厘米以……
年纪轻轻就有了抬头纹,超级显老,4个实用有效的好习惯教给你我们都被8小时睡眠论给忽悠了?不妨了解下健康睡眠的时间行走在蓝天蓝山蓝水间细节饱满OPPOReno8Pro登场,OPPOReno7跌至如果你的儿子爱发脾气,学会这9个情绪管理方法,养出一个小绅士建议中年女人,穿连衣裙一定要露出这3个地方,会更显瘦优雅我全都要!极简精致协调卡萨帝和美家族冰箱洗衣机一次满足iPhone14确认,配置全曝光,13更香了寒武纪两难活在当下还是未来?世界肾脏日吾爱吾肾知识强肾暗黑不朽手游升级有多快?轻松又减负,难怪你成功国产系统苦熬18年,却一朝归零,为何还得到国内院士的盛赞?
新疆天山北麓发现古代公共浴场遗址古代就有公共澡堂苏宁易购2021上半年营收936。06亿元,净亏损34。5亿大班数学教案香港取名最有意思的小餐馆,竟以牛奶公司命名,澳门也有!前女友伤不起啊马布里送走CBA新星,重返老东家效力,加盟宁波富邦不只是智能摄像头Aqara智能摄像机G2网关版点评哈佛咋样?热文聚热点网 提高教学质量心得体会精选让顾客100满意的技巧语言!你知道该怎么说嘛这次没答应必备小学叙事作文400字九篇

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找江西南阳嘉兴昆明铜陵滨州广东西昌常德梅州兰州阳江运城金华广西萍乡大理重庆诸暨泉州安庆南充武汉辽宁