https://blog.csdn.net/jianjun200607/article/details/86253182
https://www.jianshu.com/p/ba2ac225836d
简单介绍
Caffeine是新出现的一个高性能的Java缓存,有了它完全可以代替Guava Cache,来实现更加高效的缓存;Caffeine采用了W-TinyLFU回收策略,集合了LRU和LFU的优点,提供了一个最佳的命中率,在效率上可以秒杀Guava Cache,下面盗取一个来自网络的性能比较的截图:
如何使用
Caffeine使用非常简单,跟Guava Cache的API使用几乎一致,下面就话不多说直接,进入代码使用和学习中。
手动加载
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class CaffeineManualLoadTest {
public static void main(String[] args) {
// 手动加载
Cache
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
String key = "test1";
// 根据key查询一个缓存,如果没有则调用createTestValue方法将返回值写到缓存
// 如果createTestValue方法返回空,则get方法返回空
// 如果createTestValue方法抛出异常,则get方法返回异常
Object oj = manualCache.get(key, k -> createTestValue(k));
System.out.println("oj = " + oj);
// 将一个值写入缓存,如果存在就会覆盖掉已经存在的值
manualCache.put(key, "hello world.");
oj = manualCache.getIfPresent(key);
System.out.println("oj = " + oj);
// 删除一个缓存
manualCache.invalidate(key);
oj = manualCache.getIfPresent(key);
System.out.println("oj = " + oj);
}
private static Object createTestValue(String k) {
return null;
}
}
同步加载
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
public class CaffeineLoadingTest {
public static void main(String[] args) {
// 同步加载
LoadingCache
.expireAfterWrite(10, TimeUnit.SECONDS)
.build(key -> createTestValue(key));
String key = "test1";
// 在获取指定key的值的时候
// 如果没有获取到则通过在构建同步缓存的时候调用createTestValue方法写入方法值
Object oj = loadingCache.get(key);
System.out.println("oj : " + oj);
}
private static Object createTestValue(String k) {
return k;
}
}
异步加载
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class CaffeineAsyncLoadTest {
public static void main(String[] args) {
// 异步加载
AsyncLoadingCache
.expireAfterWrite(60, TimeUnit.SECONDS)
.buildAsync(key -> createTestValue(key));
String key = "test1";
// 查询并且在指定的key不存在的时候,通过异步的方式来构建缓存,返回的是CompletableFuture
CompletableFuture
2.基于时间:可以设置为基于秒,分等等时间策略。
3.基于引用:用到了Java中的强引用,软引用,弱引用的概念去实现的。
在本文中,我将介绍 Caffeine — 一个高性能的 Java 缓存库。
缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item。
回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 —— 缓存库的一个重要特性。
Caffeine 因使用了 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。
我们需要在 pom.xml 中添加 caffeine 依赖:
com.github.ben-manes.caffeine
caffeine
2.5.5
你可以在 Maven Central 上找到最新版本的 caffeine。
让我们来了解一下 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 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 作为参数传入。如果缓存中不存在该 key,则该函数将用于提供默认值,该值在计算后插入缓存中:
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);
这种加载缓存的方式使用了与用于初始化值的 Function 的手动策略类似的 get 方法。让我们看看如何使用它。
首先,我们需要初始化缓存:
LoadingCache 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 dataObjectMap
= cache.getAll(Arrays.asList("A", "B", "C"));
assertEquals(3, dataObjectMap.size());
从传给 build 方法的初始化函数检索值,这使得可以使用缓存作为访问值的主要门面(Facade)。
此策略的作用与之前相同,但是以异步方式执行操作,并返回一个包含值的 CompletableFuture:
AsyncLoadingCache 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,你可以在此文中获取更多内容。
Caffeine 有三个值回收策略:基于大小,基于时间和基于引用。
这种回收方式假定当缓存大小超过配置的大小限制时会发生回收。 获取大小有两种方法:缓存中计数对象,或获取权重。
让我们看看如何计算缓存中的对象。当缓存初始化时,其大小等于零:
LoadingCache 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 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());
这种回收策略是基于条目的到期时间,有三种类型:
让我们使用 expireAfterAccess 方法配置访问后过期策略:
LoadingCache 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() {
@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));
我们可以将缓存配置启用基于缓存键值的垃圾回收。为此,我们将 key 和 value 配置为 弱引用,并且可以仅配置软引用以进行垃圾回收。
当对象的没有任何强引用时,使用 WeakRefence 可以启用对象的垃圾收回收。SoftReference 允许对象根据 JVM 的全局最近最少使用(Least-Recently-Used)的策略进行垃圾回收。有关 Java 引用的更多详细信息,请参见此处。
我们应该使用 Caffeine.weakKeys()、Caffeine.weakValues() 和 Caffeine.softValues() 来启用每个选项:
LoadingCache 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 Function 计算出新值为止。
但是,如果条目可以刷新,则缓存将返回一个旧值,并异步重新加载该值。
Caffeine 有记录缓存使用情况的统计方式:
LoadingCache 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 的实现。每次与统计相关的更改将推送此对象。
在本文中,我们熟悉了 Java 的 Caffeine 缓存库,学习了如何配置和填充缓存,以及如何根据自己的需要选择适当的到期或刷新策略。