Guava中的缓存(Cache Builder)实现
概念
缓存在很多场景中是必不可少的,例如,计算或检索一个只的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。
Guava中的缓存是本地缓存的实现,与ConcurrentMap相似,但不完全一样。最基本的区别就是,ConcurrentMap会一直保存添加进去的元素,除非你主动remove掉。而Guava Cache为了限制内存的使用,通常都会设置自动回收。在某些场景下,尽管LoadingCache不回收元素,但它还是很有用的,因为它会自动加载缓存。
Guava Cache的使用场景:
- 以空间换取时间,就是你愿意用内存的消耗来换取读取性能的提升
- 你已经预测到某些数据会被频繁的查询
- 缓存中存放的数据不会超过内存空间
Guava Cache是单个应用运行时的本地缓存,单机版的缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试Memcached或Redis。
Guava Cache
Guava Cache是一个本地缓存。
优点
- 线程安全的缓存,与ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能显示的移除元素。
- 提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收。
- 监控缓存加载/命中情况。
- 集成了多部操作,调用get方式,可以在未命中缓存的时候,从其他地方获取数据源(DB,redis),并加载到缓存中。
缺点
- Cuava Cache的超时机制不是精确的
示例代码:
public static void main(String[] args) throws ExecutionException, InterruptedException{
//缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
LoadingCache<Integer,Student> studentCache
//CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
= CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置写缓存后8秒钟过期
.expireAfterWrite(8, TimeUnit.SECONDS)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
//设置要统计缓存的命中率
.recordStats()
//设置缓存的移除通知
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
}
})
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(
new CacheLoader<Integer, Student>() {
@Override
public Student load(Integer key) throws Exception {
System.out.println("load student " + key);
Student student = new Student();
student.setId(key);
student.setName("name " + key);
return student;
}
}
);
for (int i=0;i<20;i++) {
//从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据
Student student = studentCache.get(1);
System.out.println(student);
//休眠1秒
TimeUnit.SECONDS.sleep(1);
}
System.out.println("cache stats:");
//最后打印缓存的命中率等 情况
System.out.println(studentCache.stats().toString());
}
Cache
Cache是Guava提供的最基本缓存接口,创建一个Cache很简单
@Test
public void cacheCreateTest(){
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(100) //设置缓存最大容量
.expireAfterWrite(1,TimeUnit.MINUTES) //过期策略,写入一分钟后过期
.build();
cache.put("a","a1");
String value = cache.getIfPresent("a");
}
Cache是通过CacheBuilder对象来build出来的,build之前可以设置一系列的参数
LoadingCache
LoadingCache继承自Cache,当从缓存中读取某个key时,假如没有读取到值,LoadingCache会自动进行加载数据到缓存
public void loadingCacheTest() throws ExecutionException {
LoadingCache<String,String> loadingCache = CacheBuilder.newBuilder()
.maximumSize(3)
.refreshAfterWrite(Duration.ofMillis(10))//10分钟后刷新缓存的数据
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
Thread.sleep(1000);
System.out.println(key + " load data");
return key + " add value";
}
});
System.out.println(loadingCache.get("a"));
System.out.println(loadingCache.get("b"));
}
运行结果:
a load data
a add value
b load data
b add value
LoadingCache也是通过CacheBuilder创建出来的,只不过创建的时候,需要在build方法里面传入CacheLoader
CacheLoader类的load方法就是在key找不到的情况下,进行数据自动加载的
Cache常用参数
下面我们看一下Guava Cache在使用时常用的属性,下面的属性对Cache和LoadingCache都适用
1、容量初始化
public void initialCapacityTest(){
Cache<String,String> cache = CacheBuilder.newBuilder()
.initialCapacity(1024) //初始容量
.build();
}
2、最大容量
最大容量可以通过两种维度来设置
- maximumSize 最大记录数,存储数据的个数
- maximumWeight 最大容量,存储数据的大小
@Test
public void maxSizeTest(){
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(2)//缓存最大个数
.build();
cache.put("a","1");
cache.put("b","2");
cache.put("c","3");
System.out.println(cache.getIfPresent("a"));
System.out.println(cache.getIfPresent("b"));
System.out.println(cache.getIfPresent("c"));
Cache<String,String> cache1 = CacheBuilder.newBuilder()
.maximumWeight(1024 * 1024 * 1024)//最大容量为1M
//用来计算容量的Weigher
.weigher(new Weigher<String, String>() {
@Override
public int weigh(String key, String value) {
return key.getBytes().length + value.getBytes().length;
}
})
.build();
cache1.put("x","1");
cache1.put("y","2");
cache1.put("z","3");
System.out.println(cache1.getIfPresent("x"));
System.out.println(cache1.getIfPresent("y"));
System.out.println(cache1.getIfPresent("z"));
}
运行结果:
null
2
3
1
2
3
我们设置缓存的最大记录为2,当我们添加三个元素进去后,会把前面添加的元素覆盖
3、过期时间
- expireAfterWrite 写入后多长时间,数据就过期了
- expireAfterAccess 数据多长时间没有被访问,就过期
@Test
public void expireTest() throws InterruptedException {
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(100)//缓存最大个数
.expireAfterWrite(5,TimeUnit.SECONDS)//写入后5分钟过期
.build();
cache.put("a","1");
int i = 1;
while(true){
System.out.println("第" + i + "秒获取到的数据为:" + cache.getIfPresent("a"));
i++;
Thread.sleep(1000);
}
}
运行结果
第1秒获取到的数据为:1
第2秒获取到的数据为:1
第3秒获取到的数据为:1
第4秒获取到的数据为:1
第5秒获取到的数据为:1
第6秒获取到的数据为:null
第7秒获取到的数据为:null
第8秒获取到的数据为:null
第9秒获取到的数据为:null
从运行结果可以看出来,写入数据后的第6秒就开始获取不到数据了
@Test
public void expireAfterAccessTest() throws InterruptedException {
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(100)//缓存最大个数
.expireAfterAccess(5,TimeUnit.SECONDS)//5秒没有被访问,就过期
.build();
cache.put("a","1");
Thread.sleep(3000);
System.out.println("休眠3秒后访问:" + cache.getIfPresent("a"));
Thread.sleep(4000);
System.out.println("休眠4秒后访问:" + cache.getIfPresent("a"));
Thread.sleep(5000);
System.out.println("休眠5秒后访问:" + cache.getIfPresent("a"));
}
运行结果:
休眠3秒后访问:1
休眠4秒后访问:1
休眠5秒后访问:null
从运行结果可以看出,只要超过了设定的时间没有人访问,缓存的数据就会过期
缓存回收
一个残酷的现实是,我们几乎一定没有足够的内存缓存所有数据。你你必须决定:什么时候某个缓存项就不值得保留了?Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。
1.基于容量的回收(size-based eviction)
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。
另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。在权重限定场景中,除了要注意回收也是在重量逼近限定值时就进行了,还要知道重量是在缓存创建时计算的,因此要考虑重量计算的复杂度。
2.定时回收(Timed Eviction)
CacheBuilder提供两种定时回收的方法:
expireAfterAccess(long, TimeUnit)
:缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
expireAfterWrite(long, TimeUnit)
:缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
3.基于引用的回收(Reference-based Eviction)
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
CacheBuilder.weakKeys()
:使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(),使用弱引用键的缓存用而不是equals比较键。
CacheBuilder.weakValues()
:使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(),使用弱引用值的缓存用而不是equals比较值。
CacheBuilder.softValues()
:使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。
显式清除
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()
删除监听器
通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知RemovalNotification,其中包含移除原因RemovalCause、键和值。
常用方法
方法 | 作用 |
---|---|
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) | 一次获得多个键的缓存值 |
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 | 缓存加载/命中统计信息 |
引用:
https://blog.csdn.net/z5234032/article/details/52584277
https://blog.csdn.net/liuxiao723846/article/details/108392072
https://www.zhihu.com/search?type=content&q=Guava%E4%B8%ADCacheBuilder