也许这个问题有点奇怪,如果设置的存活时间为一分钟,难道不是一分钟后这个key就会立即清除掉吗?我们来分析一下如果要实现这个功能,那Cache中就必须存在线程来进行周期性地检查、清除等工作,很多cache如redis、ehcache都是这样实现的。
使用CacheBuilder构建的缓存不会”自动”执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。
这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。参考如下示例:
package com.rickiyang.learn.cache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
* @author: rickiyang
* @date: 2019/6/12
* @description:
public class GuavaCacheService {
static Cache<Integer, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
public static void main(String[] args) throws Exception {
new Thread(() -> {
while (true) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " size: " + cache.size());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}).start();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
cache.put(1, "a");
System.out.println("写入 key:1 ,value:" + cache.getIfPresent(1));
Thread.sleep(10000);
cache.put(2, "b");
System.out.println("写入 key:2 ,value:" + cache.getIfPresent(2));
Thread.sleep(10000);
System.out.println(sdf.format(new Date())
+ " sleep 10s , key:1 ,value:" + cache.getIfPresent(1));
System.out.println(sdf.format(new Date())
+ " sleep 10s, key:2 ,value:" + cache.getIfPresent(2));
部分输出结果:
23:57:36 size: 0
写入 key:1 ,value:a
23:57:38 size: 1
23:57:40 size: 1
23:57:42 size: 1
23:57:44 size: 1
23:57:46 size: 1
写入 key:2 ,value:b
23:57:48 size: 1
23:57:50 size: 1
23:57:52 size: 1
23:57:54 size: 1
23:57:56 size: 1
23:57:56 sleep 10s , key:1 ,value:null
23:57:56 sleep 10s, key:2 ,value:null
23:57:58 size: 0
23:58:00 size: 0
23:58:02 size: 0
...
...
上面程序设置了缓存过期时间为5S,每打印一次当前的size需要2S,打印了5次size之后写入key 2,此时的size为1,说明在这个时候才把第一次应该过期的key 1给删除。
给移除操作添加一个监听器:
可以为Cache对象添加一个移除监听器,这样当有记录被删除时可以感知到这个事件。
RemovalListener<String, String> listener = notification -> System.out.println("[" + notification.getKey() + ":" + notification.getValue() + "] is removed!");
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(5)
.removalListener(listener)
.build();
但是要注意的是:
默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作。
上面我们说过使用get方法的时候如果key不存在你可以使用指定方法去加载这个key。在Cache构建的时候通过指定CacheLoder的方式。如果你没有指定,你也可以在get的时候显式的调用call方法来设置key不存在的补救策略。
Cache的get方法有两个参数,第一个参数是要从Cache中获取记录的key,第二个记录是一个Callable对象。
当缓存中已经存在key对应的记录时,get方法直接返回key对应的记录。如果缓存中不包含key对应的记录,Guava会启动一个线程执行Callable对象中的call方法,call方法的返回值会作为key对应的值被存储到缓存中,并且被get方法返回。
package com.rickiyang.learn.cache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
* @author: rickiyang
* @date: 2019/6/12
* @description:
public class GuavaCacheService {
private static Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.build();
public static void main(String[] args) {
new Thread(() -> {
System.out.println("thread1");
try {
String value = cache.get("key", new Callable<String>() {
public String call() throws Exception {
System.out.println("thread1");
Thread.sleep(1000);
return "thread1";
});
System.out.println("thread1 " + value);
} catch (ExecutionException e) {
e.printStackTrace();
}).start();
new Thread(() -> {
System.out.println("thread2");
try {
String value = cache.get("key", new Callable<String>() {
public String call() throws Exception {
System.out.println("thread2");
Thread.sleep(1000);
return "thread2";
});
System.out.println("thread2 " + value);
} catch (ExecutionException e) {
e.printStackTrace();
}).start();
输出结果为:
thread1
thread2
thread2
thread1 thread2
thread2 thread2
可以看到输出结果:两个线程都启动,输出thread1,thread2,接着又输出了thread2,说明进入了thread2的call方法了,此时thread1正在阻塞,等待key被设置。然后thread1 得到了value是thread2,thread2的结果自然也是thread2。
这段代码中有两个线程共享同一个Cache对象,两个线程同时调用get方法获取同一个key对应的记录。由于key对应的记录不存在,所以两个线程都在get方法处阻塞。此处在call方法中调用Thread.sleep(1000)模拟程序从外存加载数据的时间消耗。
从结果中可以看出,虽然是两个线程同时调用get方法,但只有一个get方法中的Callable会被执行(没有打印出load2)。Guava可以保证当有多个线程同时访问Cache中的一个key时,如果key对应的记录不存在,Guava只会启动一个线程执行get方法中Callable参数对应的任务加载数据存到缓存。当加载完数据后,任何线程中的get方法都会获取到key对应的值。
可以对Cache的命中率、加载数据时间等信息进行统计。在构建Cache对象时,可以通过CacheBuilder的recordStats方法开启统计信息的开关。开关开启后Cache会自动对缓存的各种操作进行统计,调用Cache的stats方法可以查看统计后的信息。
package com.rickiyang.learn.cache;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
* @author: rickiyang
* @date: 2019/6/12
* @description:
public class GuavaCacheService {
public static void main(String[] args) {
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.recordStats()
.build();
cache.put("1", "v1");
cache.put("2", "v2");
cache.put("3", "v3");
cache.put("4", "v4");
cache.getIfPresent("1");
cache.getIfPresent("2");
cache.getIfPresent("3");
cache.getIfPresent("4");
cache.getIfPresent("5");
cache.getIfPresent("6");
System.out.println(cache.stats());
CacheStats{hitCount=3, missCount=3, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=1}