http://www.baeldung.com/java-caching-caffeine
做者:baeldung
译者:oopsguy.comjava
在本文中,我将介绍 Caffeine — 一个高性能的 Java 缓存库。git
缓存和 Map 之间的一个根本区别在于缓存能够回收存储的 item。github
回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 —— 缓存库的一个重要特性。缓存
Caffeine 因使用了 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。异步
咱们须要在 pom.xml 中添加 caffeine 依赖:maven
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.5.5</version> </dependency>
你能够在 Maven Central 上找到最新版本的 caffeine。ide
让咱们来了解一下 Caffeine 的三种缓存填充策略:手动、同步加载和异步加载。函数
首先,咱们为要缓存中存储的值类型写一个类:oop
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 方法获取值,该方法将一个参数为 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<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)。
此策略的做用与以前相同,可是以异步方式执行操做,并返回一个包含值的 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,你能够在此文中获取更多内容。
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 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());
这种回收策略是基于条目的到期时间,有三种类型:
让咱们使用 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));
咱们能够将缓存配置启用基于缓存键值的垃圾回收。为此,咱们将 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));
能够将缓存配置为在指定时间段后自动刷新条目。让咱们看看如何使用 refreshAfterWrite 方法:
Caffeine.newBuilder() .refreshAfterWrite(1, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));
这里咱们要明白 expireAfter 和 refreshAfter 之间的区别。当请求过时条目时,执行将发生阻塞,直到 build Function 计算出新值为止。
可是,若是条目能够刷新,则缓存将返回一个旧值,并异步从新加载该值。
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 的实现。每次与统计相关的更改将推送此对象。
在本文中,咱们熟悉了 Java 的 Caffeine 缓存库,学习了如何配置和填充缓存,以及如何根据本身的须要选择适当的到期或刷新策略。
文中示例的源代码能够在 Github 上找到。