4年前 (2021-03-19)  相关技术 |   抢沙发  869 
文章评分 0 次,平均分 0.0

Java高性能缓存库Caffeine简介

介绍

在本文中,我们将看一看Caffeine—一个针对Java的高性能缓存库。

缓存和映射之间的一个基本区别是缓存淘汰/过期策略

逐出策略(缓存淘汰策略)决定应在任何给定时间删除哪些对象。此策略直接影响缓存的命中率—缓存库的一个关键特性。

Caffeine使用Window TinyLfu逐出策略,它提供了一个接近最佳的命中率。

maven依赖

我们需要把Caffeine的依赖添加到我们项目的中pom.xml文件:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.5.5</version>
</dependency>

填充缓存

让我们重点讨论Caffeine的三种缓存填充策略:手动、同步加载和异步加载。

首先,让我们为我们将存储在缓存中的值类型编写一个类:

class DataObject {
    private final String data;

    private static int objectCounter = 0;
    // standard constructors/getters
    
    public static DataObject get(String data) {
        objectCounter++;
        return new DataObject(data);
    }
}

手动填充

在这个策略中,我们手动将值放入缓存,稍后再检索它们。

让我们初始化缓存:

Cache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();

现在,我们可以使用getIfPresent方法从缓存中获取一些值。如果缓存中不存在值,则此方法将返回null:

String key = "A";
DataObject dataObject = cache.getIfPresent(key);

assertNull(dataObject);

我们可以使用put方法手动填充缓存:

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);

assertNotNull(dataObject);

我们还可以使用get方法获取值,该方法将函数和键作为参数。如果缓存中不存在密钥,则此函数将用于提供回退值,该值将在计算后插入缓存:

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));

assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());

get方法以原子方式执行计算。这意味着计算将只进行一次,即使多个线程同时请求值。这就是为什么使用get比使用getIfPresent更可取的原因

有时我们需要手动使某些缓存值无效:

cache.invalidate(key);
dataObject = cache.getIfPresent(key);

assertNull(dataObject);

同步加载

这种加载缓存的方法采用一个函数,用于初始化值,类似于手动策略的get方法。让我们看看如何使用它。

首先,我们需要初始化缓存:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

现在我们可以使用get方法检索值:

DataObject dataObject = cache.get(key);

assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());

我们还可以使用getAll方法获得一组值:

Map<String, DataObject> dataObjectMap 
  = cache.getAll(Arrays.asList("A", "B", "C"));

assertEquals(3, dataObjectMap.size());

从传递给生成方法的基础后端初始化函数中检索值。这使得使用缓存作为访问值的主外观成为可能

异步加载

此策略的工作原理与前面的相同,但异步执行操作并返回一个包含实际值的CompletableFuture

AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .buildAsync(k -> DataObject.get("Data for " + k));

我们可以以相同的方式使用getgetAll方法,同时考虑到它们返回CompletableFuture的事实:

String key = "A";

cache.get(key).thenAccept(dataObject -> {
    assertNotNull(dataObject);
    assertEquals("Data for " + key, dataObject.getData());
});

cache.getAll(Arrays.asList("A", "B", "C"))
  .thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));

淘汰value

Caffeine有三种value驱逐策略:基于大小、基于时间和基于参考。

基于大小的逐出

这种类型的逐出假定在超过缓存的配置大小限制时发生逐出。获取大小的方法有两种:计算缓存中的对象的大小,或者获取它们的权重。

让我们看看如何计算缓存中的对象。初始化缓存时,其大小等于零:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(1)
  .build(k -> DataObject.get("Data for " + k));

assertEquals(0, cache.estimatedSize());

当我们添加一个值时,大小明显增加:

cache.get("A");

assertEquals(1, cache.estimatedSize());

我们可以将第二个值添加到缓存中,从而删除第一个值:

cache.get("B");
cache.cleanUp();

assertEquals(1, cache.estimatedSize());

值得一提的是,我们在获取缓存大小之前调用cleanUp方法。这是因为缓存逐出是异步执行的,此方法有助于等待逐出的完成。

我们还可以传递一个weigher函数来获取缓存的大小:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumWeight(10)
  .weigher((k,v) -> 5)
  .build(k -> DataObject.get("Data for " + k));

assertEquals(0, cache.estimatedSize());

cache.get("A");
assertEquals(1, cache.estimatedSize());

cache.get("B");
assertEquals(2, cache.estimatedSize());

当权重超过10时,这些值将从缓存中移除:

cache.get("C");
cache.cleanUp();

assertEquals(2, cache.estimatedSize());

基于时间的驱逐

此逐出策略基于条目的过期时间,有三种类型:

  • Expire after access(访问后过期)-自上次读或写操作发生后经过一段时间后,条目将过期
  • Expire after write(写入后过期)-自上次写入发生以来,在经过一段时间后,条目将过期
  • 自定义策略-到期时间由到期实现分别计算每个条目的到期时间

让我们使用expireAfterAccess方法配置ExpireAfter访问策略:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterAccess(5, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

要配置写后过期策略,我们使用expireAfterWrite方法:

cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));

要初始化自定义策略,我们需要实现Expiry接口:

cache = Caffeine.newBuilder().expireAfter(new Expiry<String, DataObject>() {
    @Override
    public long expireAfterCreate(
      String key, DataObject value, long currentTime) {
        return value.getData().length() * 1000;
    }
    @Override
    public long expireAfterUpdate(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
    @Override
    public long expireAfterRead(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
}).build(k -> DataObject.get("Data for " + k));

基于引用的逐出

我们可以将缓存配置为允许对缓存键和/或值进行垃圾收集。要做到这一点,我们需要为键和值配置WeakRefence的用法,并且我们可以为值的垃圾收集配置SoftReference

WeakRefence用法允许在没有任何对对象的强引用时对对象进行垃圾收集。SoftReference允许基于JVM的全局最近最少使用的策略对对象进行垃圾收集。

我们应该使用Caffeine.weakKeys(), Caffeine.weakValues(), 和Caffeine.softValues()启用每个选项:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));

cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .softValues()
  .build(k -> DataObject.get("Data for " + k));

刷新

可以将缓存配置为在定义的时间段后自动刷新条目。让我们看看如何使用refreshAfterWrite方法执行此操作:

Caffeine.newBuilder()
  .refreshAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

在这里,我们应该理解expireAfterrefreshAfter之间的区别。当请求过期的条目时,执行阻塞,直到生成函数计算出新值为止。

但如果条目符合刷新条件,则缓存将返回一个旧值并异步重新加载该值。

统计

Caffeine有一种记录缓存使用情况统计信息的方法:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .recordStats()
  .build(k -> DataObject.get("Data for " + k));
cache.get("A");
cache.get("A");

assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());

我们还可以传递到recordStats supplier,后者创建StatCounter的实现。每次与统计信息相关的更改都将推送此对象。

结论

在本文中,我们了解了Java的Caffeine缓存库。我们了解了如何配置和填充缓存,以及如何根据需要选择适当的过期或刷新策略。

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1732.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册