Guava-Cache

缓存是日常开发中经常应用到的一种技术手段,合理的利用缓存可以极大的改善应用程序的性能。 缓存在各种各样的用例中非常有用。例如,当计算或检索值很昂贵时,您应该考虑使用缓存,并且不止一次需要它在某个输入上的值。

maven 依赖

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。

加载

Guava Cache有两种缓存加载的方式:CacheLoader 和 Callable,这两种方式都是按照”获取缓存-如果没有-则计算”[get-if-absent-compute]的规则加载的。不同的是,CacheLoader是在创建Cache的时候,实现了一个统一的根据key获取value的方法,而Callable更加灵活,允许你在get的时候指定一个callable来获取value(get(K, Callable)

  • CacheLoader

创建CacheLoader,只需要实现load方法,如下面代码示例。当我们调用get方法获取指定key的缓存值时,如果key的value值还没有缓存,Guava Cache则会自动调用load方法加载value值到缓存并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()  
.maximumSize(1000)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});

...
try {
return graphs.get(key);
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}

从LoadingCache查询的正规方式是使用get(K)方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。由于CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionException异常。

Callable

所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式”如果有缓存则返回;否则运算、缓存、然后返回”。

1
2
3
4
5
6
7
8
9
10
11
Cache<String, String> cache = CacheBuilder.newBuilder()  
.maximumSize(1000)
.build();

// 获取某个key时,在Cache.get中单独为其指定load方法
String resultVal = cache.get("hello", new Callable<String>() {
public String call() {
String strProValue="hello world!";
return strProValue;
}
});

显式插入

使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉该key之前缓存的值。使用Cache.asMap()视图提供的任何方法也能相应的修改缓存。但是,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。

缓存回收

因为我们的数据是缓存在java堆内存上的,存储容量受到堆内存大小限制。当我们缓存的数据量很大时,会影响到GC,所以缓存必须要有回收策略。Guava Cache提供三种回收方式:

基于容量回收

通过CacheBuilder.maximumSize(long)设置缓存项的最大数目,当达到最大数目后,继续添加缓存项,Guava Cache会根据LRU策略回收缓存项来保证不超过最大数目。

另外,可以通过CacheBuilder.weigher(Weigher)设置不同缓存项的权重,Guava Cache根据权重来回收缓存项。

1
2
3
4
5
6
7
8
9
10
11
12
13
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()  
.maximumWeight(100000)
.weigher(new Weigher<Key, Graph>() {
public int weigh(Key k, Graph g) {
return g.vertices().size();
}
})
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return createExpensiveGraph(key);
}
});

定时回收

CacheBuilder提供两种定时回收的方法:

expireAfterAccess(long, TimeUnit):缓存项在给定时间范围内没有读/写访问,那么下次访问时,会被回收,然后同步load()(一个线程去load,其他线程等待)。

expireAfterWrite(long, TimeUnit):缓存项在给定时间范围内没有写访问,那么下次访问时,会被回收,然后同步load()(一个线程去load,其他线程等待)。

Guava Cache不会专门维护一个线程来回收这些过期的缓存项,只有在读/写访问时,才去判断该缓存项是否过期,如果过期,则会回收。

基于引用回收

通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

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

显式清除

1
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:

移除监听器

通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知RemovalNotification其中包含移除原因RemovalCause键和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
public DatabaseConnection load(Key key) throws Exception {
return openConnection(key);
}
};

RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>()
{
public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
DatabaseConnection conn = removal.getValue();
conn.close(); // tear down properly
}
};

return CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.removalListener(removalListener)
.build(loader);