做者:baeldunggit
轉載自公衆號:stackgcgithub
在本文中,我將介紹 Caffeine — 一個高性能的 Java 緩存庫。緩存
緩存和 Map 之間的一個根本區別在於緩存能夠回收存儲的 item。異步
回收策略爲在指定時間刪除哪些對象。此策略直接影響緩存的命中率 —— 緩存庫的一個重要特性。maven
Caffeine 因使用了 Window TinyLfu 回收策略,提供了一個近乎最佳的命中率。ide
咱們須要在 pom.xml 中添加 caffeine 依賴:函數
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>
複製代碼
你能夠在 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<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 上找到。