Caffeine快取的簡單介紹

碼農熊貓發表於2021-07-19

file

1、簡介

在本文中,我們將瞭解Caffeine,一個用於Java的高效能快取庫。

快取和Map之間的一個根本區別是快取會清理儲存的專案。

一個清理策略會決定在某個給定時間哪些物件應該被刪除,這個策略直接影響快取的命中率——快取庫的一個關鍵特性。

Caffeine使用Window TinyLfu清理策略,它提供了接近最佳的命中率。

2、依賴

我們需要將Caffeine依賴新增到我們的pom.xml中:

<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方法獲取值,該方法接受一個函式和一個鍵作為引數。如果快取中不存在該鍵,則此函式將用於提供兜底值,該值將在執行後寫入快取:

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));

assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());

這個GET方法執行是原子性的。這意味著即使多個執行緒同時請求該值,執行只會進行一次。這就是為什麼使用getgetIfPresent更好。

有時我們需要手動使一些快取的值失效:

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方法的底層後端初始化Function中讀取到,這樣就可以使用快取作為訪問值的主要入口了。

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());

當權重超過 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、基於引用的清理

我們可以配置我們的快取,允許快取的鍵或值或二者一起的垃圾收集。為此,我們需要為鍵和值配置WeakReference的使用,並且我們可以配置SoftReference僅用於值的垃圾收集。

WeakReference的使用允許在沒有對物件的任何強引用時對物件進行垃圾回收。SoftReference允許基於JVM的全域性LRU(最近最少使用)策略對物件進行垃圾回收。可以在此處找到有關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));

在這裡,我們應該明白expireAfter和refreshAfter的一個區別:當請求過期條目時,執行會阻塞,直到build函式計算出新值。但是如果該條目符合重新整理條件,則快取將返回一箇舊值並非同步重新載入該值。

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());

我們還可以建立一個StatsCounter的實現作為引數來傳入recordStats。每次與統計相關的更改,這個實現物件都將被呼叫。

7、結論

在本文中,我們熟悉了Java的Caffeine快取庫。我們看到了如何配置和存入快取,以及如何根據需要選擇合適的過期或重新整理策略。

原文:https://www.baeldung.com/java-caching-caffeine

翻譯:碼農熊貓
更多技術乾貨,請訪問我的個人網站https://pinmost.com,或關注公眾號【碼農熊貓】

相關文章