前言
“缓存”一直是我们程序员聊的最多的那一类技术点,诸如 Redis、Encache、Guava Cache,你至少会听说过一个。需要承认的是,无论是面试八股文的风气,还是实际使用的频繁度,Redis 分布式缓存的确是当下最为流行的缓存技术,但同时,从我个人的项目经验来看,本地缓存也是非常常用的一个技术点。
分析 Redis 缓存的文章很多,例如 Redis 雪崩、Redis 过期机制等等,诸如此类的公众号标题不鲜出现在我朋友圈的 timeline 中,但是分析本地缓存的文章在我的映像中很少。
在最近的项目中,有一位新人同事使用了 Guava Cache 来对一个 RPC 接口的响应进行缓存,我在 review 其代码时恰好发现了一个不太合理的写法,遂有此文。
本文将会介绍 Guava Cache 的一些常用操作:基础 API 使用,过期策略,刷新策略。并且按照我的写作习惯,会附带上实际开发中的一些总结。需要事先说明的是,我没有阅读过 Guava Cache 的源码,对其的介绍仅仅是一些使用经验或者最佳实践,不会有过多深入的解析。
先简单介绍一下 Guava Cache,它是 Google 封装的基础工具包 guava 中的一个内存缓存模块,它主要提供了以下能力:
-
封装了缓存与数据源交互的流程,使得开发更关注于业务操作
-
提供线程安全的存取操作(可以类比 ConcurrentHashMap)
-
提供常用的缓存过期策略,缓存刷新策略
-
提供缓存命中率的监控
基础使用
使用一个示例介绍 Guava Cache 的基础使用方法 -- 缓存大小写转换的返回值。
private String fetchValueFromServer(String key) {
return key.toUpperCase();
}
@Test
public void whenCacheMiss_thenFetchValueFromServer() throws ExecutionException {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return fetchValueFromServer(key);
}
});
assertEquals(0, cache.size());
assertEquals("HELLO", cache.getUnchecked("hello"));
assertEquals("HELLO", cache.get("hello"));
assertEquals(1, cache.size());
}
使用 Guava Cache 的好处已经跃然于纸上了,它解耦了缓存存取与业务操作。CacheLoader
的 load
方法可以理解为从数据源加载原始数据的入口,当调用 LoadingCache 的 getUnchecked
或者 get
方法时,Guava Cache 行为如下:
-
缓存未命中时,同步调用 load 接口,加载进缓存,返回缓存值
-
缓存命中,直接返回缓存值
-
多线程缓存未命中时,A 线程 load 时,会阻塞 B 线程的请求,直到缓存加载完毕
注意到,Guava 提供了两个 getUnchecked
或者 get
加载方法,没有太大的区别,无论使用哪一个,都需要注意,数据源无论是 RPC 接口的返回值还是数据库,都要考虑访问超时或者失败的情况,做好异常处理。
预加载缓存
预加载缓存的常见使用场景:
-
老生常谈的秒杀场景,事先缓存预热,将热点商品加入缓存;
-
系统重启过后,事先加载好缓存,避免真实请求击穿缓存
Guava Cache 提供了 put
和 putAll
方法
@Test
public void whenPreloadCache_thenPut() {
LoadingCache<String, String> cache =
CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
@Override
public String load(Stri