Guava1.3——Guava Cache进阶之同步/异步load

本文基于的guava版本是19.0

使用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中拿任务处理即可。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值