详细介绍高性能Java缓存库Caffeine

 更新时间:2018年02月02日 10:13:33   作者:oopsguy  
本篇文章主要介绍了详细介绍高性能Java缓存库Caffeine,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

1、介绍

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

缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item。

回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 — 缓存库的一个重要特征。

Caffeine 因使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。

2、依赖

我们需要在 pom.xml 中添加 caffeine 依赖:

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

您可以在Maven Central 上找到最新版本的 caffeine。

3、填充缓存

让我们来了解一下 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);
  }
}

3.1、手动填充

在此策略中,我们手动将值放入缓存之后再检索。

让我们初始化缓存:

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 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该键,则该函数将用于提供回退值,该值在计算后插入缓存中:

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

3.2、同步加载

这种加载缓存的方法使用了与用于初始化值的 Function 相似的手动策略的 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());

从传递给 build 方法的底层后端初始化函数检索值。 这使得可以使用缓存作为访问值的主要门面(Facade)。

3.3、异步加载

此策略的作用与之前相同,但是以异步方式执行操作,并返回一个包含值的 CompletableFuture:

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

我们可以以相同的方式使用 get 和 getAll 方法,同时考虑到他们返回的是 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()));

CompletableFuture 有许多有用的 API,您可以在此文中获取更多内容。

4、值回收

Caffeine 有三个值回收策略:基于大小,基于时间和参考。

4.1、基于大小回收

这种回收方式假定当超过配置的缓存大小限制时会发生回收。 获取大小有两种方法:缓存中计数对象,或获取权重。

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

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 Function 来获取缓存的大小:

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

当 weight 超过 10 时,值将从缓存中删除:

cache.get("C");
cache.cleanUp();
 
assertEquals(2, cache.estimatedSize());

4.2、基于时间回收

这种回收策略是基于条目的到期时间,有三种类型:

  1. 访问后到期 — 从上次读或写发生后,条目即过期。
  2. 写入后到期 — 从上次写入发生之后,条目即过期
  3. 自定义策略 — 到期时间由 Expiry 实现独自计算

让我们使用 expireAfterAccess 方法配置访问后过期策略:

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

4.3、基于引用回收

我们可以将缓存配置为启用缓存键值的垃圾回收。为此,我们将 key 和 value 配置为 弱引用,并且我们可以仅配置软引用以进行垃圾回收。

当没有任何对对象的强引用时,使用 WeakRefence 可以启用对象的垃圾收回收。SoftReference 允许对象根据 JVM 的全局最近最少使用(Least-Recently-Used)的策略进行垃圾回收。有关 Java 引用的更多详细信息,请参见此处

我们应该使用 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));

5、刷新

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

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

这里我们应该要明白 expireAfter 和 refreshAfter 之间的区别。 当请求过期条目时,执行将发生阻塞,直到 build Function 计算出新值为止。

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

6、统计

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,创建一个 StatsCounter 的实现。每次与统计相关的更改将推送此对象。

7、结论

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

文中示例的源代码可以在 Github 上找到。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Java简单计算两个日期月数差的方法

    Java简单计算两个日期月数差的方法

    这篇文章主要介绍了Java简单计算两个日期月数差的方法,结合实例形式分析了java使用Calendar类进行日期时间操作相关技巧,需要的朋友可以参考下
    2017-06-06
  • Idea报错: A JNI error has occurred的问题

    Idea报错: A JNI error has occurred的问题

    这篇文章主要介绍了Idea报错: A JNI error has occurred的问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • 这一次搞懂Spring事务注解的解析方式

    这一次搞懂Spring事务注解的解析方式

    这篇文章主要介绍了这一次搞懂Spring事务注解的解析方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • SpringBoot Redis清除所有的key的实现方法

    SpringBoot Redis清除所有的key的实现方法

    本文主要介绍了SpringBoot Redis清除所有的key的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • 基于Java代码操作Redis过程详解

    基于Java代码操作Redis过程详解

    这篇文章主要介绍了基于Java代码操作Redis过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • springboot连接kafka集群的使用示例

    springboot连接kafka集群的使用示例

    在项目中使用kafka的场景有很多,尤其是实时产生的数据流,本文主要介绍了springboot连接kafka集群的使用示例,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • Java实现读取resources目录下的文件路径的九种方式

    Java实现读取resources目录下的文件路径的九种方式

    本文主要介绍了Java实现读取resources目录下的文件路径的九种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • Java上传文件错误java.lang.NoSuchMethodException的解决办法

    Java上传文件错误java.lang.NoSuchMethodException的解决办法

    今天小编就为大家分享一篇关于Java上传文件错误java.lang.NoSuchMethodException的解决办法,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • Java设计模式中单一职责原则详解

    Java设计模式中单一职责原则详解

    这篇文章主要介绍了Java设计模式中单一职责原则详解,单一职责原则 (SRP) 是软件设计中的一个重要原则,它要求每个类只负责一个职责,需要的朋友可以参考下
    2023-05-05
  • 基于Java Swing制作一个Pong小游戏

    基于Java Swing制作一个Pong小游戏

    《Pong》是美国雅达利公司(ATARI)开发的视频游戏,该作模拟了两个打乒乓球的人,就是在两条线中间有一个点在动,操纵器就是一个摇杆上有一个按钮的那种。本文就来用Java Swing制作一个Pong小游戏吧
    2023-01-01

最新评论