缓存分为本地缓存和远端缓存。常见的远端缓存有 Redis,MongoDB;本地缓存一般使用 map 的方式保存在本地内存中。
一般在业务中操作缓存,都会操作缓存和数据源两部分。
如:put 数据时,先插入 DB,再删除原来的缓存;get 数据时,先查缓存,若未命中,则查询 DB,再将结果放入缓存中。并发时,还得兼顾本地缓存的线程安全问题。必要的时候也要考虑缓存的回收策略。
而 Guava Cache 是 Google Guava 中的一个内存缓存模块,用于将数据缓存到 JVM 内存中,很好的解决了上面提到的几个问题:
- 简单的 get、put 操作,能够集成数据源 ;
- 线程安全的缓存,与 ConcurrentMap 相似,并且增加了失效策略;
- 提供了三种基本的缓存回收方式:容量回收、定时回收和引用回收。其中定时回收有两种:按写入时间(最早写入的最先回收);按访问时间(最早访问的最早回收);
- 监控缓存加载/命中情况。
Guava Cache 存储的是键值对的集合,主要实现的缓存功能有:
- 自动将数据加载至缓存结构中,当缓存的数据超过最大值时,使用LRU算法替换;
- 具备根据节点上一次被访问或写入时间计算缓存过期机制;
- 还可以统计缓存使用过程中的命中率、异常率和命中率等统计数据。
一、基本使用
首先导入 maven 依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
下面展示了使用 Guava Cache 创建一个缓存对象并使用它,目标是缓存全班学生姓名。
其中 nameCache
的 key 是班级的 id,value 是一个 list,存储学生姓名。
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class GuavaCacheService {
/**
* 缓存操作对象
*/
private final LoadingCache<String, List<String>> nameCache = loadCache(new CacheLoader<String, List<String>>() {
@Override
public List<String> load(String key) {
// 从数据库加载,当key对应的value不存在时的处理逻辑
return databaseManager.getNamesByClassId(key);
}
});
/**
* 缓存设置
* 缓存项最大数量:10000
* 缓存有效时间(天):1
*/
private LoadingCache<String, List<String>> loadCache(CacheLoader<String, List<String>> cacheLoader) {
return CacheBuilder.newBuilder()
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
// 设置初始容量
.initialCapacity(1000)
// 缓存池大小
.maximumSize(10000)
// 设置 设定时间 后 刷新缓存
.refreshAfterWrite(1, TimeUnit.DAYS)
.build(cacheLoader);
}
/**
* 设置缓存值
* 若已有该key值,则会先移除(会触发removalListener移除监听器),再添加
*/
public void put(String key, List<String> value) {
if (StringUtils.isBlank(key) || value == null) {
return;
}
try {
nameCache.put(key, value);
} catch (Exception e) {
log.info("设置缓存值出错", e);
}
}
/**
* 获取缓存值
* 如果键不存在值,将调用CacheLoader的load方法,将load方法的返回值加载到缓存
*/
public List<String> get(String key) {
List<String> token = null;
try {
token = nameCache.get(key);
} catch (Exception e) {
log.info("获取缓存值出错", e);
}
return token;
}
}
在调用 CacheBuilder 的 build
方法时,须传递一个 CacheLoader 类型的参数,CacheLoader 的 load
方法需要我们提供实现。当调用 LoadingCache 的 get
方法时,如果不存在 key 的记录,则会自动调用 load
方法加载数据,load
方法的返回值会作为 key 对应的 value 存储到 LoadingCache中,并从 get 方法返回。
CacheBuilder 是 Guava 提供的一个快速构建缓存对象的工具类。CacheBuilder 类采用 builder 设计模式,它的每个方法都返回 CacheBuilder 本身,直到 build 方法被调用。 该类中提供了很多的参数设置选项,可以设置 cache 的默认大小,并发数,存活时间,过期策略等等。
二、配置分析
缓存的并发级别
Guava 提供了设置并发级别的 api,使得缓存支持并发的写入和读取。同 ConcurrentHashMap 类似 Guava cache 的并发也是通过分离锁实现。在一般情况下,将并发级别设置为服务器 cpu 核心数是一个比较不错的选择。
CacheBuilder.newBuilder()
// 设置并发级别为cpu核心数
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.build();
缓存的初始容量设置
在构建缓存时,可以设置一个合理大小初始缓存容量。由于 Guava 的缓存使用了分离锁的机制,扩容的代价昂贵,所以合理的初始容量能够减少缓存容器的扩容次数。
CacheBuilder.newBuilder()
// 设置初始容量为100
.initialCapacity(100)
.build();
设置最大存储
指定缓存能够存储的最大记录数量。当 Cache 中的记录数量达到最大值后,再调用 put 方法添加对象时,Guava 会先从当前缓存中删除一条,腾出空间后再将新的对象存储到 Cache 中。
- 基于容量的清除:通过
CacheBuilder.maximumSize()
方法设置 Cache 的最大容量数,当缓存数量达到或接近该最大值时,将清除最近最少使用的缓存; - 基于权重的清除:使用
CacheBuilder.weigher()
指定一个权重函数,并且用CacheBuilder.maximumWeight()
指定最大总重。比如每一项缓存所占据的内存空间大小都不一样,可以看作它们有不同的“权重”。
缓存清除策略
1. 基于存活时间的清除
- expireAfterWrite 写缓存后多久过期
- expireAfterAccess 读写缓存后多久过期
- refreshAfterWrite 写入数据后多久过期,只阻塞当前数据加载线程,其他线程返回旧值
这几个策略时间可以单独设置,也可以组合配置。建议使用 refreshAfterWrite。
这里需要注意刷新策略和回收策略不太一样:刷新表示为 key 加载新值,这个过程可以是异步的(需要重写CacheLoader的reload方法,否则仍然是同步的调用load),而回收 key 的时候,会调用 load 方法加载值,这个过程是同步的。
网友评论