Guava Cache【译】

1. 概述

在这篇文章中,我们将看一下Guava Cache的实现,包括基本用法,收回策略,缓存刷新和一些很有趣的批量操作。

最后,我们将看看如何使用删除缓存通知。

2. 如何使用Guava Cache

让我们从一个简单的例子开始 – 让我们缓存大写形式的String实例。

首先我们创建一个CacheLoader,用来计算缓存中存储的值。由此,我们将使用方便的CacheBuilderr根据给定的规范来创建缓存:

@Test
public void whenCacheMiss_thenValueIsComputed() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().build(loader);
 
    assertEquals(0, cache.size());
    assertEquals("HELLO", cache.getUnchecked("hello"));
    assertEquals(1, cache.size());
}

注意到,这时候我们的key“hello”中并没有值。所以这个值被计算后保存到缓存中。

我们还注意到,我们使用了getUnchecked()操作,这会将目前不存在的值计算并存储在缓存中。

3. 收回策略

3.1. 通过size

我们可以通过使用maximumSize()方法来限制缓存的大小。如果缓存达到了限制值,那么最早的项目将被收回。

在下面的代码中,我们将缓存的大小限制为3条:

@Test
public void whenCacheReachMaxSize_thenEviction() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().maximumSize(3).build(loader);
 
    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("forth");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("first"));
    assertEquals("FORTH", cache.getIfPresent("forth"));
}

3.2. 通过Weight

我们也可以通过设置权重来限制缓存大小。

在下面的代码中,我们使用长度作为自定义权重函数:

@Test
public void whenCacheReachMaxWeight_thenEviction() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
 
    Weigher<String, String> weighByLength;
    weighByLength = new Weigher<String, String>() {
        @Override
        public int weigh(String key, String value) {
            return value.length();
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .maximumWeight(16)
      .weigher(weighByLength)
      .build(loader);
 
    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("last");
    assertEquals(3, cache.size());
    assertNull(cache.getIfPresent("first"));
    assertEquals("LAST", cache.getIfPresent("last"));
}

注意:缓存会移除不止一条记录来给新的大记录留出空间。

3.3. 通过Time

除了通过size来移出旧的记录,我们还可以通过时间。在下面的例子中,我们自定义缓存删除闲置2ms以上的记录:

@Test
public void whenEntryIdle_thenEviction()
  throws InterruptedException {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .expireAfterAccess(2,TimeUnit.MILLISECONDS)
      .build(loader);
 
    cache.getUnchecked("hello");
    assertEquals(1, cache.size());
 
    cache.getUnchecked("hello");
    Thread.sleep(300);
 
    cache.getUnchecked("test");
    assertEquals(1, cache.size());
    assertNull(cache.getIfPresent("hello"));
}

我们也可以基于他们的总存活时间来收回记录。在下面的例子中,缓存会移除存储后2ms的数据:

@Test
public void whenEntryLiveTimeExpire_thenEviction()
  throws InterruptedException {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .expireAfterWrite(2,TimeUnit.MILLISECONDS)
      .build(loader);
 
    cache.getUnchecked("hello");
    assertEquals(1, cache.size());
    Thread.sleep(300);
    cache.getUnchecked("test");
    assertEquals(1, cache.size());
    assertNull(cache.getIfPresent("hello"));
}

4. 弱键

接下来我们看看看如何使我们的缓存键可以被弱引用 – 在他们在任何地方都没有被引用时,允许垃圾回收器回收这些缓存键。

默认情况下,缓存的key和value是强引用关系,但是我们可以使用weakKeys() 方法来使缓存使用弱引用储存这些key,如下代码所示:

@Test
public void whenWeakKeyHasNoRef_thenRemoveFromCache() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().weakKeys().build(loader);
}

5. 软值

我们也可以通过softValues() 方法来使垃圾回收器可以回收我们缓存中的值,如下代码所示:

@Test
public void whenSoftValue_thenRemoveFromCache() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().softValues().build(loader);
}

注意:太多的软引用可能会影响系统性能 – 建议使用maximumSize()。

6. 处理null值

现在我们来看下如何处理缓存中的null值。默认情况下,Guava Cache会在你试图加载一个null值时抛出异常 – 缓存一个null值并不合理。

但是如果null值在你的代码中有意义。那么你可以充分利用Optional类,如下代码所示:

@Test
public void whenNullValue_thenOptional() {
    CacheLoader<String, Optional<String>> loader;
    loader = new CacheLoader<String, Optional<String>>() {
        @Override
        public Optional<String> load(String key) {
            return Optional.fromNullable(getSuffix(key));
        }
    };
 
    LoadingCache<String, Optional<String>> cache;
    cache = CacheBuilder.newBuilder().build(loader);
 
    assertEquals("txt", cache.getUnchecked("text.txt").get());
    assertFalse(cache.getUnchecked("hello").isPresent());
}
private String getSuffix(final String str) {
    int lastIndex = str.lastIndexOf('.');
    if (lastIndex == -1) {
        return null;
    }
    return str.substring(lastIndex + 1);
}

7. 刷新缓存

接下来,让我们看看如果刷新缓存的值。我们可以通过使用refreshAfterWrite()方法自动刷新缓存。

在下面的示例中,缓存会每一分钟自动刷新:

@Test
public void whenLiveTimeEnd_thenRefresh() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .refreshAfterWrite(1,TimeUnit.MINUTES)
      .build(loader);
}

注意:你可以使用refresh(key)方法手动刷新特定的纪录。

8. 预加载缓存

我们可以使用putAll()方法在缓存中插入多个记录。在下面的示例中,我们使用Map将多个记录添加到缓存中:

@Test
public void whenPreloadCache_thenUsePutAll() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(String key) {
            return key.toUpperCase();
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder().build(loader);
 
    Map<String, String> map = new HashMap<String, String>();
    map.put("first", "FIRST");
    map.put("second", "SECOND");
    cache.putAll(map);
 
    assertEquals(2, cache.size());
}

9. 移除通知

有些情况下,你需要在缓存纪录被删除的时候做一些操作,所以我们讨论一下RemovalNotification。

我们可以注册一个RemovalListener来获取纪录被删除的通知。我们也可以获取到删除的原因 – 通过getCause()方法。

在下面的例子中,当在缓存中第四个对象因为他的长度被删除时会收到一个通知:

@Test
public void whenEntryRemovedFromCache_thenNotify() {
    CacheLoader<String, String> loader;
    loader = new CacheLoader<String, String>() {
        @Override
        public String load(final String key) {
            return key.toUpperCase();
        }
    };
 
    RemovalListener<String, String> listener;
    listener = new RemovalListener<String, String>() {
        @Override
        public void onRemoval(RemovalNotification<String, String> n){
            if (n.wasEvicted()) {
                String cause = n.getCause().name();
                assertEquals(RemovalCause.SIZE.toString(),cause);
            }
        }
    };
 
    LoadingCache<String, String> cache;
    cache = CacheBuilder.newBuilder()
      .maximumSize(3)
      .removalListener(listener)
      .build(loader);
 
    cache.getUnchecked("first");
    cache.getUnchecked("second");
    cache.getUnchecked("third");
    cache.getUnchecked("last");
    assertEquals(3, cache.size());
}

10. 注意

最后,这里有几个关于Guava缓存实现的一些其他说明:

  • 它是线程安全的
  • 你可以使用put(key,value)将值手动插入到缓存中
  • 你可以使用CacheStats ( hitRate(), missRate(), ..)方法衡量缓存性能

11. 总结

在本教程中,我们介绍了很多Guava缓存的用例-从简单用法到收回元素,刷新和预加载缓存以及删除通知。

原文链接

发表评论

电子邮件地址不会被公开。 必填项已用*标注