Contents

Guava 缓存简介

1. 概述

在本教程中,我们将了解Guava Cache的实现——基本用法、删除策略、刷新缓存和一些有趣的批量操作。

最后,我们将看看使用缓存能够发送的删除通知。

2. 如何使用 Guava 缓存

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

首先,我们将创建CacheLoader——用于计算存储在缓存中的值。由此,我们将使用方便的CacheBuilder使用给定的规范构建我们的缓存:

@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());
}

请注意,我们的“hello”键在缓存中没有值——因此该值被计算和缓存。

另请注意,我们正在使用*getUnchecked()*操作 - 如果该值不存在,它会计算并将值加载到缓存中。

3. 删除政策

每个缓存都需要在某个时候删除值。让我们讨论一下从缓存中逐出值的机制——使用不同的标准。

3.1. 按大小删除

我们可以使用*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. 按权重删除

我们还可以使用自定义权重函数来**限制缓存大小。**在下面的代码中,我们使用长度作为我们的自定义权重函数:

@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. 按时间删除

除了使用大小来删除旧记录,我们还可以使用时间。在以下示例中,我们自定义缓存以删除已空闲 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. 弱键

接下来,让我们看看如何使我们的缓存键具有弱引用——允许垃圾收集器收集其他地方没有引用的缓存键。

默认情况下,缓存键和值都具有强引用,但我们可以使用*weakKeys()*使缓存使用弱引用存储键,如下例所示:

@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值。默认情况下,如果您尝试加载null值, Guava Cache将抛出异常——因为缓存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. 刷新缓存

接下来,让我们看看如何刷新我们的缓存值。

7.1. 手动刷新

我们可以在*LoadingCache.refresh(key)*的帮助下手动刷新单个键。

String value = loadingCache.get("key");
loadingCache.refresh("key");

这将强制CacheLoader加载键的新值。

在成功加载新值*之前,该key的先前值将由*get(key)返回。

7.2. 自动刷新

我们可以使用*CacheBuilder.refreshAfterWrite(duration)*来自动刷新缓存值。

@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);
}

重要的是要了解*refreshAfterWrite(duration)仅使密钥在指定的 duration 之后符合刷新条件。只有当get(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()*方法访问删除的原因。

在以下示例中,当缓存中的第四个元素因其大小而被接收时,会收到RemovalNotification

@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(), ..)*来衡量您的缓存性能