场景

Java核心工具库Guava介绍以及Optional和Preconditions使用进行非空和数据校验:

​​Java核心工具库Guava介绍以及Optional和Preconditions使用进行非空和数据校验_霸道流氓气质的博客-博客​​

在上面引入Guava的基础上。学习其本地缓存Cache的使用。

缓存在很多场景下都是相当有用的。例如,计算或检索一个值的代价很高,

并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。

通常来说,Guava Cache 适用于:

1、你愿意消耗一些内存空间来提升速度。

2、你预料到某些键会被查询一次以上。

3、缓存中存放的数据总量不会超出内存容量。(Guava Cache 是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。)

注:

博客:
​​霸道流氓气质的博客_博客-C#,架构之路,SpringBoot领域博主​​ 关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。

Guava的缓存Cache创建和存取

Guava的缓存创建需要通过CacheBuilder的build()方法构建,它的每个方法都返回CacheBuilder本身,

直到build方法被调用才会创建Cache或者LoadingCache

//模拟从数据库中查询数据
public User getUserByName(String name){
return User.builder().name(name).age(20).build();
}
@Test
public void test1(){

//Guava的缓存创建需要通过CacheBuilder的build()方法构建,它的每个方法都返回CacheBuilder本身,直到build方法被调用才会创建Cache或者LoadingCache
LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
//maximumSize 设置最大存储条数
.maximumSize(1000)
.build(
new CacheLoader<String, User>() {
@Override
public User load(String name) throws Exception {
//缓存加载逻辑,比如查询数据库等
return getUserByName(name);
}
}
);

User user = null;
try {
//从LoadingCache查询使用get方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值
user = userCache.get("霸道的程序猿");
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(user);//User(name=霸道的程序猿, age=20)

Cache<String, String> build = CacheBuilder.newBuilder().maximumSize(100).build();
//放入缓存
build.put("a","1");
//获取缓存,如果缓存不存在则返回一个null值
System.out.println(build.getIfPresent("a"));//1

//所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K,Callable)方法,这个方法返回缓存中的值,如果
//缓存中没有,则通过Callable进行加载并返回,该操作是原子
//这个方法简便地实现了模式“如果有缓存则返回;否则运算、缓存、然后返回”
try {
String badao = build.get("badao", new Callable<String>() {
@Override
public String call() throws Exception {
return "霸道的程序猿";
}
});
System.out.println(badao);//霸道的程序猿
} catch (ExecutionException e) {
e.printStackTrace();
}
}

Guava Cache缓存回收-基于容量回收

基于容量回收
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)
在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常发生在缓存项的数目逼近限定值时
另外不同的缓存项有不同的“权重”(weights)-例如:如果你的缓存值,占据完全不同的内存空间,
可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重

@Test
public void test2() throws InterruptedException {

//Guava Cache提供了三种基本的缓存回收方式
//1、基于容量回收
//如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)
//在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常发生在缓存项的数目逼近限定值时
//另外不同的缓存项有不同的“权重”(weights)-例如:如果你的缓存值,占据完全不同的内存空间,
//可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重
Cache<String, User> userCache = CacheBuilder
.newBuilder()
//设置最大存储容量
.maximumWeight(10)
.weigher(new Weigher<String, User>() {
@Override
public int weigh(String s, User user) {
//按照年龄大小作为权重计算方式
return user.getAge();
}
}).build();

int i = 1;

while (true){
userCache.put(i+"",User.builder().name(i+"").age(i).build());
ConcurrentMap<String, User> stringUserConcurrentMap = userCache.asMap();
System.out.println(stringUserConcurrentMap);
i++;
Thread.sleep(1000);
}

//运行结果
// {1=User(name=1, age=1)}
// {2=User(name=2, age= 2),1=User(name=1, age=1)}
// {2=User(name=2, age= 2),3=User(name=3, age= 3),1=User(name=1, age=1)}
// {2=User(name=2, age= 2),3=User(name=3, age= 3),4=User(name=4, age= 4),1=User(name=1, age=1)}
// {5=User(name=5, age= 5),4=User(name=4, age=4)}
// {6=User(name=6, age=6)}
// {7=User(name=7, age=7)}
// {8=User(name=8, age=8)}
// {9=User(name=9, age=9)}
}

Guava Cache缓存回收-定时回收

CacheBuilder提供两种定时回收的方法:
expireAfterAccess(long,TimeUnit):缓存项在给定时间内没有被读/写访问,则回收
expireAfterWriter(long,TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖)

Cache<Object, Object> build = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterAccess(5, TimeUnit.SECONDS)
.build();

build.put("a","a");
while (true){
System.out.println(build.asMap());
Thread.sleep(1000);
}

//输出结果
// {a=a}
// {a=a}
// {a=a}
// {a=a}
// {a=a}
// {}
// {}

Guava Cache缓存回收-手动回收

invalidate(key)回收个别指定
invalidataeAll(keys)批量回收
invalidataeAll()回收所有

Cache<Object, Object> build = CacheBuilder.newBuilder()
.build();
for (int i = 0; i < 10 ; i++) {
build.put(i,i);
if(i % 2 == 0){
build.invalidate(i);
}
}
System.out.println(build.asMap());//{5=5, 7=7, 1=1, 9=9, 3=3}
build.invalidateAll();
System.out.println(build.asMap());//{}

}

Guava Cache缓存回收-基于引用

//        4、通过weakKeys和weakValues方法指定Cache只保存对缓存记录key和value的弱引用。
//        这样当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收。
//
//        CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
//       因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
//        CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。
//       因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
//        CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。
//        考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。
//        使用软引用值的缓存同样用==而不是equals比较值。

Cache<Object, Object> build = CacheBuilder.newBuilder()
.weakKeys()
.build();
}

Guava Cache添加移除监听器

可以为Cache对象添加一个移除监听器,以便缓存被移除时做一些额外操作。
缓存项被移除时,RemovalListener会获取通知RemovalNotification,其中包含移除原因RemovalCause、键和值

Cache<String, String> build = CacheBuilder.newBuilder()
.maximumSize(3)
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> notification) {
//输出结果:cause:EXPLICIT key:a value:a被移除
System.out.println("cause:"+notification.getCause()+" key:"+notification.getKey()+" value:"+notification.getValue()+"被移除");
}
})
.build();
build.put("a","a");
build.invalidate("a");

Guava Cache刷新

刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。

在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。

Guava Cache 定时刷新

在进行缓存定时刷新时,需要指定缓存的刷新间隔,和一个用来加载缓存的CacheLoader,当达到刷新时间间隔后,

下一次获取缓存时,会调用CacheLoader的load方法刷新缓存

LoadingCache<String, String> build = CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
return s+LocalDateTime.now().toString();
}
});

while (true){
System.out.println(build.get("s"));
Thread.sleep(1000);
}
//输出结果
// s2022-11-20T13:50:53.369
// s2022-11-20T13:50:53.369
// s2022-11-20T13:50:53.369
// s2022-11-20T13:50:53.369
// s2022-11-20T13:50:53.369
// s2022-11-20T13:50:58.429
// s2022-11-20T13:50:58.429
// s2022-11-20T13:50:58.429

注意:

缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)

因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置

如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也会变得可以回收

Guava Cache 异步刷新

在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的操作必须等待新值加载完成。

ExecutorService executor = Executors.newFixedThreadPool(1);
LoadingCache<String, String> build = CacheBuilder.newBuilder()
.refreshAfterWrite(6, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
Thread.sleep(2000);
return s+LocalDateTime.now().toString();
}

@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
ListenableFutureTask<String> task = ListenableFutureTask.create(()->load(key));
executor.execute(task);
return task;
}
});

while (true){
System.out.println(build.get("a"));
Thread.sleep(1000);
}

//模拟客户端一秒调用一次,设置每6秒刷新数据,每次刷新数据需要2秒,在刷新数据期间,获取的仍然是旧值
//输出结果
// a2022-11-20T14:46:46.135
// a2022-11-20T14:46:46.135
// a2022-11-20T14:46:46.135
// a2022-11-20T14:46:46.135
// a2022-11-20T14:46:46.135
// a2022-11-20T14:46:46.135
// a2022-11-20T14:46:46.135
// a2022-11-20T14:46:46.135
// a2022-11-20T14:46:54.207
// a2022-11-20T14:46:54.207
// a2022-11-20T14:46:54.207
// a2022-11-20T14:46:54.207
// a2022-11-20T14:46:54.207
// a2022-11-20T14:46:54.207
// a2022-11-20T14:46:54.207
// a2022-11-20T14:46:54.207
// a2022-11-20T14:46:54.207
// a2022-11-20T14:47:02.288
// a2022-11-20T14:47:02.288
// a2022-11-20T14:47:02.288

Guava Cache开启统计

CacheBuilder.recordStats()用来开启 Guava Cache 的统计功能。

统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

hitRate():缓存命中率;

averageLoadPenalty():加载新值的平均时间,单位为纳秒;

evictionCount():缓存项被回收的总数,不包括显式清除。

ExecutorService executor = Executors.newFixedThreadPool(1);
LoadingCache<String, String> build = CacheBuilder.newBuilder()
.refreshAfterWrite(6, TimeUnit.SECONDS)
//开启统计
.recordStats()
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
Thread.sleep(2000);
return s+LocalDateTime.now().toString();
}

@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
ListenableFutureTask<String> task = ListenableFutureTask.create(()->load(key));
executor.execute(task);
return task;
}
});

while (true){
System.out.println(build.get("a"));
System.out.println("命中率:"+build.stats().hitCount());
System.out.println("加载损耗的平均时间:"+build.stats().averageLoadPenalty());
System.out.println("回收次数:"+build.stats().evictionCount());
Thread.sleep(1000);
}

//输出结果
// a2022-11-21T10:16:25.759
// 命中率:0
// 加载损耗的平均时间:2.0633746E9
// 回收次数:0
// a2022-11-21T10:16:25.759
// 命中率:1
// 加载损耗的平均时间:2.0633746E9
// 回收次数:0
// a2022-11-21T10:16:25.759
// 命中率:2
// 加载损耗的平均时间:2.0633746E9
// 回收次数:0
// a2022-11-21T10:16:25.759
// 命中率:3
// 加载损耗的平均时间:2.0633746E9
// 回收次数:0
// a2022-11-21T10:16:25.759
// 命中率:4
// 加载损耗的平均时间:2.0633746E9
// 回收次数:0

Guava Cache asMap视图

asMap 视图提供了缓存的 ConcurrentMap 形式,但 asMap 视图与缓存的交互需要注意:

1、cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;

2、asMap().get(key)实质上等同于 cache.getIfPresent(key),而且不会引起缓存项的加载。这和 Map 的语义约定一致。

3、所有读写操作都会重置相关缓存项的访问时间,包括 Cache.asMap().get(Object)方法和 Cache.asMap().put(K, V)方法,

但不包括 Cache.asMap().containsKey(Object)方法,也不包括在 Cache.asMap()的集合视图上的操作。

比如,遍历 Cache.asMap().entrySet()不会重置缓存项的读取时间。