Guava (一)Guava Cache进阶之同步/异步load

使用guava cache的时候,在cache中没有值或者值需要更新的时候,都需要去load,而这个load往往对应从数据库或者远程接口拿数据并缓存下来的操作。在高qps场景的服务中,这个load可能会导致调用链的阻塞,如果阻塞时间长,可能会影响服务,甚至可能拖垮服务,所以要了解哪些地方会阻塞,有没有什么方法能够尽量少的去阻塞。

一.同步load
1.load
load是第一次加载,加载之前cache中没有值。load永远都是同步的,不管是否使用异步进行包装,具体见下面第三部分。

对于同一个key,多次请求只会触发一次加载。比如,线程1访问key1,发现cache中不存在key1,然后触发key1的load。与此同时,在load加载完成之前,线程2,线程3...线程N都访问key1,这些访问不会再触发key1的加载,但是在key1的load加载完成之前,这些请求都会被hang在那里等待。调用链如下:

get:4952, LocalCache$LocalLoadingCache

getOrLoad:3967, LocalCache

get:3963, LocalCache

get:2046, LocalCache$Segment

lockedGetOrLoad:2140, LocalCache$Segment

waitForLoadingValue:2153, LocalCache$Segment

waitForValue:3571, LocalCache$LoadingValueReference

getUninterruptibly:168, Uninterruptibles->这里用future的get方法阻塞在这里,直到第一个触发load操作的线程完成load操作并将结果设置到该future中,这个get方法就会解除阻塞并获取到load到的值。

2.reload
reload是之前cache中有值,需要刷新该值,比如设置了过期时间后,到了缓存过期需要更新的时间,会触发scheduleRefresh去做刷新。手动调用refresh方法的时候也会触发reload。

3.refresh
手动调用了refresh,会导致loadingcache的重新load操作。调用的是Segment中的refresh方法,里面有loadAsync方法,LoadingValueReference的loadFuture中会根据之前cache中是否有值来决定load或者reload。

load永远都是同步的,不管你是否使用异步包装:

reload如果是被异步包装过的,那么就会是异步操作的,否则和load一样也是同步的:

注意:

正如上面描述的,load永远都是阻塞的,是因为虽然这里的方法名是loadAsync,但是如果loadFuture中当前key的value是空的,那么调用loader.load方法的时候,还是同步阻塞的,返回的future是在阻塞操作拿到结果之后才把future返回的,这时future已经有值了。只有reload的时候才是异步操作。

4.refreshAfterWrite
guava cache的所有更新操作都是依靠读写方法触发的,因为其内部没有时钟或者定时任务。比如上一次写之后超过了refresh设置的更新时间,但是之后没有cache的访问了,那么下次get的时候才会触发refresh,这次触发会导致get的阻塞。

调用链路:

{@link com.google.common.cache.LocalCache.Segment#scheduleRefresh}
{@link com.google.common.cache.LocalCache.Segment#loadAsync}
{@link com.google.common.cache.LocalCache.LoadingValueReference#loadFuture}
{@link CacheLoader#reload}

注意:只有当前触发load的get方法会被阻塞!

这里会有检测,如果当前key已经在loading状态,那么refresh直接返回null,不会阻塞。

而loading又需要时间,所以在loading完毕之前,其他get方法拿到的都是旧值。

二.异步load
CacheLoader.asyncReloading

 这里的reload方法被包装成了一个future,内部用任务将reload操作包装为异步操作,所以在reload的时候会调用被封装为异步的方法:

 这里调用不会被阻塞,所以即使是触发get方法,因为是封装为异步任务,所以也不会被阻塞。

但是load方法不同于reload,它还是阻塞的。

所以这里的异步优化,相对于上面的同步reload,只减少了“一个”线程的阻塞。

三.工程设计上的一些实践和考量
工程上我们在使用guava cache的时候,一般都会使用get方法,这样在key不存在的时候会触发加载,后续的请求访问到这个key的时候就会可以取到缓存的内容了。

这样的使用方式无可厚非,同时也是最直观最能够想到的cache使用方式,但是我们考虑这样几个情况:

高并发场景,同时load的加载需要一定的时间

如果我们忽视这个点,可能会导致服务在启动的时候load很高,同时上游系统可能会发现调用该系统的超时请求量增加。我们假设我们系统一般处理请求需要50ms的时间,单个线程每秒就可以处理20个请求。如果load的时间是50ms,同时这20个请求都访问的同一个key,那么这20个请求,每个请求的的处理时间会从50ms涨到100ms,那么势必会影响该系统的吞吐。

load中有远程api调用依赖

同时,如果load的实现是一个远程调用,无论是读外部接口,还是读数据库,都不是我们自己能控制的,一旦外部接口响应变慢,会导致该cache的get操作都卡住,很可能造成雪崩。

所以首先,如果在load中存在远程调用,一定要设置好超时时间。

其次,如果对于key请求的结果,设计一定的降级策略(如果业务允许的话),比如访问cache的时候使用getIfPresent,如果发现没有值,就使用降级策略的默认值,同时异步去用get或者refresh触发该key的加载过程,这样对于cache做到读写线程分离,让读的操作永远不会阻塞。

如果再追求一点极致的设计的话,线程池异步触发load的操作也可以从读(线程)动作中解耦出来,我们可以使用queue作为中介,这样线程池异步触发load的操作进一步简化为“封装load操作为task,放入queue中”,节省了在读线程中有可能触发的启动线程的动作,同时只需要一个后台线程(池)去这个queue中拿任务处理即可。

==================================

核心类

CacheBuilder:类,缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。
CacheBuilder在build方法中,会把前面设置的参数,全部传递给LocalCache,它自己实际不参与任何计算。这种初始化参数的方法值得借鉴,代码简洁易读。
CacheLoader:抽象类。用于从数据源加载数据,定义load、reload、loadAll等操作。
Cache:接口,定义get、put、invalidate等操作,这里只有缓存增删改的操作,没有数据加载的操作。
AbstractCache:抽象类,实现Cache接口。其中批量操作都是循环执行单次行为,而单次行为都没有具体定义。
LoadingCache:接口,继承自Cache。定义get、getUnchecked、getAll等操作,这些操作都会从数据源load数据。
AbstractLoadingCache:抽象类,继承自AbstractCache,实现LoadingCache接口。
LocalCache:类。整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法。
LocalManualCache:LocalCache内部静态类,实现Cache接口。
其内部的增删改缓存操作全部调用成员变量 localCache(LocalCache类型)的相应方法。
LocalLoadingCache:LocalCache内部静态类,继承自LocalManualCache类,实现LoadingCache接口。
其所有操作也是调用成员变量localCache(LocalCache类型)的相应方法。
CacheStats:缓存加载/命中统计信息。

常用方法

V getIfPresent(Object key) 获取缓存中key对应的value,如果缓存没命中,返回null。
V get(K key) throws ExecutionException 获取key对应的value,若缓存中没有,则调用LocalCache的load方法,从数据源中加载,并缓存。
void put(K key, V value) 如果缓存有值,覆盖,否则,新增
void putAll(Map m);循环调用单个的方法
void invalidate(Object key); 删除缓存
void invalidateAll(); 清楚所有的缓存,相当远map的clear操作。
long size(); 获取缓存中元素的大概个数。为什么是大概呢?元素失效之时,并不会实时的更新size,所以这里的size可能会包含失效元素。
CacheStats stats(); 缓存的状态数据,包括(未)命中个数,加载成功/失败个数,总共加载时间,删除个数等。
asMap()方法获得缓存数据的ConcurrentMap快照
cleanUp()清空缓存
refresh(Key) 刷新缓存,即重新取缓存数据,更新缓存
ImmutableMap getAllPresent(Iterable keys) 一次获得多个键的缓存值
 

参考:https://blog.csdn.net/xxcupid/article/details/92834521

https://blog.csdn.net/u012187218/article/details/79309202


 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值