[译]高性能缓存库Caffeine介绍及实践

概览

本文咱们将介绍Caffeine-一个Java高性能缓存库。缓存和Map之间的一个根本区别是缓存会将储存的元素逐出。逐出策略决定了在什么时间应该删除哪些对象,逐出策略直接影响缓存的命中率,这是缓存库的关键特征。Caffeine使用Window TinyLfu逐出策略,该策略提供了接近最佳的命中率。java

添加依赖

首先在pom.xml文件中添加Caffeine相关依赖:git

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.5.5</version>
</dependency>
复制代码

您能够在Maven Central上找到最新版本的Caffeine。github

缓存填充

让咱们集中讨论Caffeine的三种缓存填充策略:手动,同步加载和异步加载。缓存

首先,让咱们建立一个用于存储到缓存中的DataObject类:异步

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);
    }
}
复制代码

手动填充

在这种策略中,咱们手动将值插入缓存中,并在后面检索它们。ide

让咱们初始化缓存:函数

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方法手动将值插入缓存:ui

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);
 
assertNotNull(dataObject);
复制代码

咱们还可使用get方法获取值,该方法将Lambda函数和键做为参数。若是缓存中不存在此键,则此Lambda函数将用于提供返回值,而且该返回值将在计算后插入缓存中:atom

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));
 
assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());
复制代码

get方法以原子方式(atomically)执行计算。这意味着计算将只进行一次,即便多个线程同时请求该值。这就是为何使用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());
复制代码

从传递给build方法的初始化函数中检索值。这样就能够经过缓存在来装饰访问值。

异步加载

该策略与先前的策略相同,可是异步执行操做,并返回保存实际值的CompletableFuture:

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

考虑到它们返回CompletableFuture的事实,咱们能够以相同的方式使用get和getAll方法:

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,您能够在本文中了解更多信息。

逐出元素

Caffeine具备三种元素逐出策略:基于容量,基于时间和基于引用。

基于容量的逐出

这种逐出发生在超过配置的缓存容量大小限制时。有两种获取容量当前占用量的方法,计算缓存中的对象数量或获取它们的权重。

让咱们看看如何处理缓存中的对象。初始化高速缓存时,其大小等于零:

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 — 自上次写入以来,在通过过时时间以后该元素过时。
  • Custom policy — 经过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));
复制代码

基于引用的逐出

咱们能够将缓存配置为容许垃圾回收缓存的键或值。为此,咱们将为键和值配置WeakRefence的用法,而且咱们只能为值的垃圾收集配置为SoftReference。

当对象没有任何强引用时,WeakRefence用法容许对对象进行垃圾回收。 SoftReference容许根据JVM的全局“最近最少使用”策略对对象进行垃圾收集。有关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));
复制代码

刷新缓存

能够将缓存配置为在定义的时间段后自动刷新元素。让咱们看看如何使用refreshAfterWrite方法执行此操做:

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

在这里,咱们应该了解expireAfter和refreshAfter之间的区别。前者当请求过时元素时,执行将阻塞,直到build()计算出新值为止。

可是后者将返回旧值并异步计算出新值并插入缓存中,此时被刷新的元素的过时时间将从新开始计时计算。

统计

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传递给它,recordStats建立StatsCounter的实现。每次与统计相关的更改都将推送给此对象。

总结

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

原文地址:www.baeldung.com/java-cachin…


🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟

欢迎访问笔者博客:blog.dongxishaonian.tech

关注笔者公众号,推送各种原创/优质技术文章 ⬇️

WechatIMG6
相关文章
相关标签/搜索