[譯] 高效能 Java 快取庫 — Caffeine

StackGC發表於2017-10-25

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

作者:baeldung

轉載自公眾號:stackgc

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 作為引數傳入。如果快取中不存在該 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);
複製程式碼

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));
複製程式碼

我們可以以相同的方式使用 getgetAll 方法,同時考慮到他們返回的是 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、基於時間回收

這種回收策略是基於條目的到期時間,有三種型別:

  • 訪問後到期 — 從上次讀或寫發生後,條目即過期。
  • 寫入後到期 — 從上次寫入發生之後,條目即過期
  • 自定義策略 — 到期時間由 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));
複製程式碼

這裡我們要明白 expireAfterrefreshAfter 之間的區別。當請求過期條目時,執行將發生阻塞,直到 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 上找到。

相關文章