清除什么时候发生?
也许这个问题有点奇怪,如果设置的存活时间为一分钟,难道不是一分钟后这个key就会立即清除掉吗?我们来分析一下如果要实现这个功能,那Cache中就必须存在线程来进行周期性地检查、清除等工作,很多cache如redis、ehcache都是这样实现的。
但在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清除),偶尔在读操作时做(如果写操作实在太少的话),也就是说在使用的是调用线程,参考如下示例:
代码
public class CacheService {
static Cache<Integer, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
public static void main(String[] args) throws Exception {
new Thread() { //monitor
public void run() {
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, "Hi");
System.out.println("write key:1 ,value:"+cache.getIfPresent(1));
Thread.sleep(10000);
// when write ,key:1 clear
cache.put(2, "bbb");
System.out.println("write key:2 ,value:"+cache.getIfPresent(2));
Thread.sleep(10000);
// when read other key ,key:2 do not clear
System.out.println(sdf.format(new Date())
+" after write, key:1 ,value:"+cache.getIfPresent(1));
Thread.sleep(2000);
// when read same key ,key:2 clear
System.out.println(sdf.format(new Date())
+" final, key:2 ,value:"+cache.getIfPresent(2));
}
}
控制台输出
00:34:17 size: 0
write key:1 ,value:Hi
00:34:19 size: 1
00:34:21 size: 1
00:34:23 size: 1
00:34:25 size: 1
write key:2 ,value:bbb
00:34:27 size: 1
00:34:29 size: 1
00:34:31 size: 1
00:34:33 size: 1
00:34:35 size: 1
00:34:37 after write, key:1 ,value:null
00:34:37 size: 1
00:34:39 final, key:2 ,value:null
00:34:39 size: 0
通过分析发现:
(1)缓存项<1,"Hi">的存活时间是5秒,但经过5秒后并没有被清除,因为还是size=1
(2)发生写操作cache.put(2, “bbb”)后,缓存项<1,"Hi">被清除,因为size=1,而不是size=2
(3)发生读操作cache.getIfPresent(1)后,缓存项<2,"bbb">没有被清除,因为还是size=1,看来读操作确实不一定会发生清除
(4)发生读操作cache.getIfPresent(2)后,缓存项<2,"bbb">被清除,因为读的key就是2
这在GuavaCache被称为“延迟删除”,即删除总是发生得比较“晚”,这也是GuavaCache不同于其他Cache的地方!这种实现方式的问题:缓存会可能会存活比较长的时间,一直占用着内存。如果使用了复杂的清除策略如基于容量的清除,还可能会占用着线程而导致响应时间变长。但优点也是显而易见的,没有启动线程,不管是实现,还是使用起来都让人觉得简单(轻量)。
如果你还是希望尽可能的降低延迟,可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp(),ScheduledExecutorService可以帮助你很好地实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。
总结
请一定要记住GuavaCache的实现代码中没有启动任何线程!!Cache中的所有维护操作,包括清除缓存、写入缓存等,都是通过调用线程来操作的。这在需要低延迟服务场景中使用时尤其需要关注,可能会在某个调用的响应时间突然变大。
GuavaCache毕竟是一款面向本地缓存的,轻量级的Cache,适合缓存少量数据。如果你想缓存上千万数据,可以为每个key设置不同的存活时间,并且高性能,那并不适合使用GuavaCache。