示例

  1. LoadingCahce<Key, Graph> graphs = CacheBuilder.newBuilder()
  2. .maximumSize(1000)
  3. .expireAfterWrite(10, TimeUnit.MINUTES)
  4. .removalListener(MY_LISTENER)
  5. .build(new CacheLoader<Key, Graph>() {
  6. public Graph load(Key key) throws AnyException {
  7. return createExpensiveGraph(key);
  8. }
  9. });

应用场景

缓存有着非常广泛的使用场景,并且作用也是非常惊人的。比如一个值的计算或获取的代价是很昂贵的,那么就应该考虑来缓存它,后续可能不止一次的使用到。

Cache很类似于ConcurrentMap,但又不完全一样。最基本的不同之处在于ConcurrentMap持久化所有元素直到它们被移除,而Cache是被配置为自动回收的,用来限制内存的使用。某些情况下,LoadingCache的自动缓存加载非常有用,即使它不回收内容。

总之,Guava的缓存适用于以下情况:

  • 希望用空间换取时间,即划出一部分内存用户缓存,来换取加载速度
  • keys不止一次被查询
  • 缓存数据不会存储在除了内存的其他地方(Guava的缓存应用本地单例的。它们不会往文件或其他外部服务器写入数据,如果这些不能满足需求,请考虑使用Memcached)

如果你有以上的使用场景,那么Guava的缓存工具将会是你正确的选择。

Cache是通过建造者模式使用CacheBuilder来获取的,像上面的例子那样。自定义缓存是比较有趣的一部分。

注意,如果你不需要Cache的功能,那么ConcurrentMap的内存效率将会更高,但是任何ConcurrentMap都不能或很难拥有Cache的功能。

Population

关于缓存你需要问自己的第一问题是:有没有实用的默认函数来加载或计算与一个key关联的值?如果有,那么你需要使用CacheLoader。如果没有,或者你需要覆写默认的“获取如果不存在”的行为,那么你需要传入一个Callable来接受回调。可以使用Cache.put直接插入元素,但是对于所有缓存的内容来说,自动缓存加载会更加容易。

CacheLoader加载

LoadingCache是一个通过CacheLoader构建出来的Cache,创建一个CacheLoader非常容易,实现V load(K key) throws Exception方法,例如,可以通过下列方法创建一个LoadingCache

  1. LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder().maximumSize(100).build(new CacheLoader<Key, Graph>() {
  2. public Graph load(Key k) throws AnyException {
  3. return createExpansiveGraph(k);
  4. }
  5. });
  6. ...
  7. try {
  8. return graphs.get(key);
  9. } catch(EcecutionException e) {
  10. throw new OtherException(e.getCause());
  11. }

查询LoadingCache的权威方法是通过get(K)方法,要么返回一个已缓存的值,要么使用CacheLoader来加载一个新的值。CacheLoader可能会抛出异常,LoadingCache.get(K)将会抛出ExecutionException。如果定义了不抛出经检查的异常,在获取的时候可以使用getUnchecked(K)来查询缓存。注意不要使用getUnchecked(K)方法来查询定义了检查异常的CacheLoader

  1. LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build(new CacheLoader<Key, Graph>() {
  2. public Graph load(Key k) {
  3. return createExpansiveGraph(k);
  4. }
  5. });
  6. ...
  7. return graphs.getUnchecked(key);

批量查询可以使用getAll(Iterable<? extends K>)。默认情况下,getAll对于每一个不存在于缓存中的key都将触发一个单独的CacheLoader.load调用。当批量获取比单独获取跟家高效的时候,可以覆写CacheLoader.loadAll()改变这个行为。getAll的性能会有相应的提高。

Callable加载

所有Guava的缓存,加载或不加载的,都支持get(K, Callable<V>)。这个方法返回与此key相关联的值,或从Callable中计算并添加到缓存中。直到加载完毕Cache的状态才会改变。这个方法使用了一种简单的方式来代替了“如果有,返回;如果没有,创建、存储并返回”模式。

  1. Cache<Key, Value> cache = CacheBuilder.newBuilder().maximumSize(1000).build(); //没有CacheLoader
  2. try {
  3. cache.get(key, new Callable() {
  4. public Value call() throws AnyException {
  5. return doThingsTheHardWay();
  6. }
  7. })
  8. } catch (ExecutionException e) {
  9. throw new OtherException(e.getCause());
  10. }

直接插入

可以使用Cache.put(key, value)直接把值插入到缓存中。会覆盖该key之前关联的值。使用Cache.asMap()视图暴露的ConcurrentMap的方法也会同步改变,但是不会自动加载任何不存在的key-value,任何在该视图上的原子的操作都在自动缓存加载的作用域外。所以不管是使用CacheLoader还是Callable来加载不存在的key-value,尽量优先使用Cache.get(Key, Callable<V>)而不是Cache.asMap().putIfAbsent()

回收

一个残酷的现实是我们不可能使用内存来缓存所有想缓存的数据。必须决定谁将先从缓存中删除。Guava提供了基本的回收类型:基于内存占用大小的回收,基于时间的回收和基于引用的回收。

基于内存占用大小的回收

如果不希望缓存占用过大的内存空间,查看CacheBuilder.maximumSize(long)。最近最久未使用的key-value将被回收。注意:Cache可能在尚未达到此限制的时候就进行回收了,尤其当大小接近限制的时候。

另外,如果不同的缓存具有不同的大小,例如,缓存的值内存模型完全不同,可能就需要指定CacheBuilder.weigher(Wheigher)CacheBuilder.maximumWeight(long)。与maximumSize不同,weight在创建的时候就已经计算了,之后一直保持不变。

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

基于时间的回收

Guava提供了两种基于时间的回收方式:

  • expireAfterAccess(long, TimeUnit),仅在最后一次读写访问之后在指定的时间后才会回收,回收顺序类似于基于大小的回收。
  • expireAfterWrite(long, TimeUnit),在key-value被创建、修改后经过指定的时间后被回收,这对于一段时间后缓存的数据已经无效的情况下非常有用。

测试基于时间的回收

基于时间的回收的测试没有多麻烦,测试一个2秒回收的缓存并不需要实际等上2秒。使用Ticker接口和CacheBuilder.ticker(Ticker)方法来指定一个时间源,来代替系统的时钟。

基于引用的回收

Guava可以使用key或value的弱引用、value的弱引用来设置缓存以允许垃圾回收器回收entry。

  • CacheBuilder.weakKeys()用弱引用来存储keys,允许垃圾回收器回收不再被其他变量引用(强引用或软引用)的key的entry。因为垃圾回收器使用地址相等性(==)来判断两个对象是否一致,所以这就会导致所有缓存使用==而不是equals()来判断key的相等性。
  • CacheBuilder.weakValues()同上(keys改为values)。
  • CacheBuilder.softValues()使用软引用存储values。软引用是一种全局的最近最少使用的对象的回收机制。因为软引用的性能潜在的问题,推荐使用基于大小的回收。该方法同样会导致所有缓存使用==而不是equals()来判断value的相等性。

显式的删除

任何时候,都可以显式的刷新缓存而不必等待其被回收,通过以下方式:

  • 单个的移除,使用Cache.invalidate(key)
  • 批量的移除,使用Cache.invalidateAll(keys)
  • 移除所有,使用Cache.invalidateAll()

移除监听器

在ebtry被移除的时候可以设置一个监听器来做一些额外的操作:CacheBuilder.removalListener(RemovalListener),监听器会收到一个RemovalNotification参数,该参数指定了RemovalCause,包含key和value。

RemovalListener所有抛出的异常都会被记录和接收。

  1. CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection>() {
  2. public DatabaseConnection load(Key k) throws Exception {
  3. return openConnection(k);
  4. }
  5. };
  6. RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
  7. public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
  8. DatabaseConnection conn = removal.getValue();
  9. conn.close();
  10. }
  11. };
  12. return CacheBuilder.newBuilder().expireAfter(2, TimeUnit.MINUTES).removalListener(removalListener).build(loader);

注意RemovalListener默认是同步执行的,因为缓存的维护是在缓存的操作中进行的,所以大量的操作会减慢缓存的功能。如果在移除监听器中有大量的操作,使用RemovalListeners.asynchronous(RemovalListener, Executor)包装一个RemovalListener来执行异步操作。

清理工作何时执行?

CacheBuilder构建的缓存不会“自动的”或在values过期后立即执行清理回收工作。相应的,在写操作后,或在写操作很少的情况下,在读操作后,会进行小量的维护工作。

原因如下:如果我们需要持续的维护缓存,就需要创建一个线程,那么它的操作就会和用户的操作为共享的锁产生竞争。另外一些环境也限制了线程的创建,会让CacheBuilder变的不可用。

但是我们让用户做出选择。如果你的缓存需要高吞吐量,那么就不需要担心清理过期的entry来进行缓存的维护操作;如果很少需要写操作,并且不希望清理操作阻塞读操作,那么可以创建自己的清理线程,并在其中间隔性的调用Cache.cleanup()

如果需要为一个不经常进行写操作的Cache安排常规的缓存维护计划,使用ScheduledExecutorService

刷新

刷新和回收不完全一致。像LoadingCache.refresh(K),刷新key会为该key加载一个新值(可能是异步的)。旧值会在该key刷新时返回,与回收相反(回收会强制在值过期后才会去重新获取)。

如果在刷新的时候抛出了异常,旧值会保留,异常被记录并且接收(不处理也不向上抛)。

CacheLoader可以通过覆写CacheLoader.reload(K, V)在刷新时指定更加敏捷的行为 — 使用旧值计算新值。

  1. LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder().maximumSize(1000).refreshAfterWrite(1, TimeUnit.MINUTES).build(new CacheLoader<Key, Graph>() {
  2. public Graph load(Key k) {
  3. return getGraphFromDatabase(k);
  4. }
  5. public ListenableFuture<Graph> reload(final Key k, Graph prevGraph) {
  6. if (neverNeedsRefresh) {
  7. return Futures.immediateFuture(preGraph);
  8. } else {
  9. //异步
  10. ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
  11. public Graph call() {
  12. return getGraphFromDatabase(k);
  13. }
  14. });
  15. executor.execute(task);
  16. return task;
  17. }
  18. }
  19. });

可以通过CacheBuilder.refreshAfterWrite(long, TimeUnit)为缓存加上自动刷新。与expireAfterWrite()相反,refreshAfterWrite()能在一段时间后让某个key自动刷新,但是这个刷新动作仅在查询时才会发生(如果CacheLoader.reload()是异步实现的,刷新不会减慢查询的速度)。例如,可以在一个Cache上同时指定refreshAfterWrite()expireAfterWrite(),在任何可以刷新的情况下,过期的计时器不会盲目的重置,而是在可以刷新之后却没有查询操作,此时才会真的过期。

功能

统计

使用CacheBuilder.recordStats(),可以打开Guava缓存的统计功能。Cache.stats()方法返回一个CacheStats对象,提供了以下统计功能:

  • hitRate()返回请求的次数
  • averageLoadPenalty()加载新值的平均时间,单位是纳秒
  • evictionCount()缓存回收次数

除此之外还有很多统计方法。这些统计在缓存调整上非常严格,建议在有严格性能要求的程序上留意这些统计信息。

asMap

在任何Cache上使用asMapConcurrentMap视图查看缓存,但是asMapCache是怎么交互的需要说明一下:

  • Cache.asMap包含了当前Cache中已加载的全部entry。例如Cache.asMap().keySet()包含所有已加载的keys。
  • asMap().get(key)本质上相当于Cache.getIfAbsent(key),并且不会导致值的加载,这与Map的限制是一致的。
  • 读写都会重置访问的时间(包括Cache.asMap().get(Object)Cache.asMap().put(K, V)),但是不包含containsKey(Object)和其他asMap()视图操作。例如迭代Cache.entrySet()就不会重置访问的时间。

中断

加载方法(像get)不会抛出InterruptedException,之前曾经这么做过但是我们的支持尚未到位,它的好处只有一点但是成本偏高,如需详细信息请继续阅读。

get方法将请求未缓存的数据分为两大类:加载值和等待其他线程内加载值。//TODO (有点难)