GuavaCache使用笔记

在高并发的应用系统中,缓存是提高性能的一把利器。
常用到的缓存技术有分布式缓存,像Redis、MC;也有本地缓存,像ehcache、guava cache等。这里说的是本地缓存guava cache。
在缓存使用中,像缓存数据一致性和缓存数据的更新敏感度问题需要着重考虑。这些与缓存的更新策略密切相关。
针对本地缓存,更新方法有很多种,比如最常用的:
被动更新: 是先从缓存获取,没有则回源取,再放回缓存;
主动更新: 数据改变后主动更新缓存(在多机环境下,可以采用消息通知的方式更新)。
在高并发场景下,被动更新的回源是要格外小心的;因为缓存穿透可以引发雪崩效应: 如果有太多请求在同一时间回源,后端服务如果无法支撑这么高并发,容易引发后端服务崩溃。如果使用Guava Cache就能很好避免雪崩问题,Guava Cache里的CacheLoader在回源的load方法上加了控制,对于同一个key,只让一个请求回源load,其他线程阻塞等待结果。同时,在Guava里可以通过配置expireAfterAccess/expireAfterWrite设定key的过期时间,key过期后就单线程回源加载并放回缓存。这样通过Guava Cache简简单单就较为安全地实现了缓存的被动更新操作。
为什么是"较为安全"呢?因为如果同一时间仍有太多的不同key过期,还是会有大量请求穿透缓存而请求到后端服务上,仍然有可能使后端服务崩溃,有什么办法解决这个问题呢?
1.将key的过期时间加个随机值,避免大家一起过期(前提是对业务不影响)。
2.自己控制回源的并发数,即使有一万个key要更新,也只让100个可以回源,其余的9900个等着,(可以通过Guava的Striped实现)。
3.在过期前主动更新,更新完成后将过期时间延长。

另外,如果对刚才说的对于同一个key,只让一个请求回源,其他线程等待觉得还不爽,虽然对后端服务不会造成压力,但我的请求都还是blocked了,整个请求还是会被堵一下。对此Guava Cache还提供了一个refreshAfterWrite的配置项,定时刷新数据,刷新时仍只有一个线程回源取数据,但其他线程只会稍微等一会,没等到就返回旧值,整个请求看起来就比较平滑了。为什么又是“比较平滑”呢?因为默认的刷新回源线程是同步的,如果想达到全过程平滑的效果,可以将刷新回源线程做成异步方式。这样数据的更新都是在后台异步做了,但这样也是有一定的代价的,比如过了刷新时间,仍可能拿到旧值,直到拿回数据更新缓存后才会返回新值。因为这个refresh是惰性刷新,并不是主动发起的: 比如设置了5秒refresh一下,Guava的做法并不是真的每5秒刷一次,而是等请求到了之后,发现需要refresh时才会真的更新。所以,这一点需要注意,比如虽然设置了5秒刷新,但如果超过1分钟都没有请求(假设key没有过期),当1分零1秒有请求来时,仍有可能返回旧值。


以下是关于采用Expire和Refresh(sync/async)Guava Cache对请求回源的处理示意图:

 

Guava Cache还有一些其他的配置,比如设置initCapacity,concurrencyLevel(默认4),weakKeys,maximumSize等。
GuavaCache的数据清理方式: 缓存数据量超过Segment的maximumSize后的清理算法是LRU(非全局),而且是put/store等方法操作时同步清理,至数量少于maximumSize为止。Segment的maximumSize = Total maximumSize / Segment number(concurrentLevel),意味着如果在CacheBuilder里配置的maximumSize是400,concurrentLevel为4,那么Segment的maximumSize是100=400÷4,所以这个LRU清理并不是全局的LRU,而是Segment范围内的LRU清理,发生清理时缓存里的数据总量也不一定是严格超过maximumSize的。



你可能感兴趣的:(Cache)