Einführung in Koffein

Einführung in Koffein

1. Einführung

In diesem Artikel werfen wir einen Blick aufCaffeine - ahigh-performance caching library for Java.

Ein grundlegender Unterschied zwischen einem Cache und einemMap besteht darin, dass ein Cache gespeicherte Elemente entfernt.

Eineviction policy decides which objects should be deleted zu einem bestimmten Zeitpunkt. Diese Richtliniedirectly affects the cache’s hit rate - ein entscheidendes Merkmal für das Zwischenspeichern von Bibliotheken.

Koffein verwendet die Räumungsrichtlinie vonWindow TinyLfu, dienear-optimal hit rate liefert.

2. Abhängigkeit

Wir müssen die Abhängigkeit voncaffeinezu unserenpom.xmlhinzufügen:


    com.github.ben-manes.caffeine
    caffeine
    2.5.5

Sie finden die neueste Version voncaffeineon Maven Central.

3. Cache füllen

Konzentrieren wir uns auf diethree strategies for cache population von Koffein: manuelles, synchrones und asynchrones Laden.

Schreiben wir zunächst eine Klasse für die Wertetypen, die wir in unserem Cache speichern:

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. Manuelles Auffüllen

Bei dieser Strategie werden Werte manuell in den Cache gestellt und später abgerufen.

Initialisieren wir unseren Cache:

Cache cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();

Nunwe can get some value from the cache using the getIfPresent method. Diese Methode gibtnull zurück, wenn der Wert nicht im Cache vorhanden ist:

String key = "A";
DataObject dataObject = cache.getIfPresent(key);

assertNull(dataObject);

Wir könnenpopulate the cache manuell mit der Methodeput ausführen:

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);

assertNotNull(dataObject);

We can also get the value using the get method, wobei einFunction zusammen mit einem Schlüssel als Argument verwendet wird. Diese Funktion wird zum Bereitstellen des Fallback-Werts verwendet, wenn der Schlüssel nicht im Cache vorhanden ist, der nach der Berechnung in den Cache eingefügt würde:

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

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

Dieget-Methode führt die Berechnung atomar durch. Dies bedeutet, dass die Berechnung nur einmal durchgeführt wird - auch wenn mehrere Threads gleichzeitig nach dem Wert fragen. Deshalbusing get is preferable to getIfPresent.

Manchmal müssen wirinvalidate some cached values manuell ausführen:

cache.invalidate(key);
dataObject = cache.getIfPresent(key);

assertNull(dataObject);

3.2. Synchrones Laden

Diese Methode zum Laden des Caches benötigtFunction,, die zum Initialisieren von Werten verwendet wird, ähnlich der Methodeget der manuellen Strategie. Mal sehen, wie wir das nutzen können.

Zunächst müssen wir unseren Cache initialisieren:

LoadingCache cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

Jetzt können wir die Werte mit der Methodegetabrufen:

DataObject dataObject = cache.get(key);

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

Wir können auch eine Reihe von Werten mit der MethodegetAllerhalten:

Map dataObjectMap
  = cache.getAll(Arrays.asList("A", "B", "C"));

assertEquals(3, dataObjectMap.size());

Die Werte werden aus der zugrunde liegenden Back-End-InitialisierungFunction abgerufen, die an die Methodebuild übergeben wurde. This makes it possible to use the cache as the main facade for accessing values.

3.3. Asynchrones Laden

Diese Strategieworks the same as the previous but performs operations asynchronously and returns a CompletableFuturehält den tatsächlichen Wert:

AsyncLoadingCache cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .buildAsync(k -> DataObject.get("Data for " + k));

Wir könnenuse the get and getAll methods auf die gleiche Weise berücksichtigen, wobei wir die Tatsache berücksichtigen, dass sieCompletableFuture zurückgeben:

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 verfügt über eine umfangreiche und nützliche API, in der Sie mehr überin this article lesen können.

4. Räumung von Werten

Koffein hatthree strategies for value eviction: größenbasiert, zeitbasiert und referenzbasiert.

4.1. Größenbasierte Räumung

Diese Art der Räumung setzt voraus, dasseviction occurs when the configured size limit of the cache is exceeded. Es gibttwo ways of getting the size - Objekte im Cache zählen oder ihre Gewichte abrufen.

Mal sehen, wie wircount objects in the cache können. Wenn der Cache initialisiert wird, ist seine Größe gleich Null:

LoadingCache cache = Caffeine.newBuilder()
  .maximumSize(1)
  .build(k -> DataObject.get("Data for " + k));

assertEquals(0, cache.estimatedSize());

Wenn wir einen Wert hinzufügen, nimmt die Größe offensichtlich zu:

cache.get("A");

assertEquals(1, cache.estimatedSize());

Wir können den zweiten Wert zum Cache hinzufügen, wodurch der erste Wert entfernt wird:

cache.get("B");
cache.cleanUp();

assertEquals(1, cache.estimatedSize());

Es ist erwähnenswert, dass wircall the cleanUp method before getting the cache size. Dies liegt daran, dass die Cache-Räumung asynchron ausgeführt wird und diese Methodehelps to await the completion of the eviction.

Wir können auchpass a weigherFunction verwenden, um die Größe des Caches zu ermitteln:

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

Die Werte werden aus dem Cache entfernt, wenn das Gewicht über 10 liegt:

cache.get("C");
cache.cleanUp();

assertEquals(2, cache.estimatedSize());

4.2. Zeitbasierte Räumung

Diese Räumungsstrategie istbased on the expiration time of the entry und hat drei Arten:

  • Expire after access - Der Eintrag ist abgelaufen, nachdem die Zeit seit dem letzten Lesen oder Schreiben verstrichen ist

  • Expire after write - Der Eintrag ist nach Ablauf der Zeit seit dem letzten Schreibvorgang abgelaufen

  • Custom policy - Eine Ablaufzeit wird für jeden Eintrag einzeln durch die Implementierung vonExpiryberechnet

Konfigurieren Sie die Expire-After-Access-Strategie mithilfe derexpireAfterAccess-Methode:

LoadingCache cache = Caffeine.newBuilder()
  .expireAfterAccess(5, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

Um die Expire-After-Write-Strategie zu konfigurieren, verwenden wir die MethodeexpireAfterWrite:

cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));

Um eine benutzerdefinierte Richtlinie zu initialisieren, müssen wir dieExpiry-Schnittstelle implementieren:

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

4.3. Referenzbasierte Räumung

Wir können unseren Cache so konfigurieren, dassgarbage-collection of cache keys and/or values zulässig sind. Zu diesem Zweck konfigurieren wir die Verwendung vonWeakRefence sowohl für Schlüssel als auch für Werte und konfigurieren dieSoftReference nur für die Speicherbereinigung von Werten.

Die Verwendung vonWeakRefenceermöglicht die Speicherbereinigung von Objekten, wenn keine starken Verweise auf das Objekt vorhanden sind. SoftReference ermöglicht das Sammeln von Objekten auf der Grundlage der globalen Strategie der JVM, die am wenigsten verwendet wurde. Weitere Details zu Referenzen in Java finden Sie unterhere.

Wir solltenCaffeine.weakKeys(),Caffeine.weakValues(), undCaffeine.softValues() verwenden, um jede Option zu aktivieren:

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

5. Erfrischend

Es ist möglich, den Cache so zu konfigurieren, dass Einträge nach einem definierten Zeitraum automatisch aktualisiert werden. Mal sehen, wie das mit der MethoderefreshAfterWritegeht:

Caffeine.newBuilder()
  .refreshAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

Hier sollten wir adifference between expireAfter and refreshAfter verstehen. Wenn der abgelaufene Eintrag angefordert wird, wird eine Ausführung blockiert, bis der neue Wert durch den BuildFunction berechnet worden wäre.

Wenn der Eintrag jedoch für die Aktualisierung geeignet ist, gibt der Cache einen alten Wert undasynchronously reload the value zurück.

6. Statistiken

Koffein hat ein Mittel vonrecording statistics about cache usage:

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

Wir können auch anrecordStats Lieferanten übergeben, wodurch eine Implementierung vonStatsCounter. erstellt wird. Dieses Objekt wird bei jeder statistischen Änderung verschoben.

7. Fazit

In diesem Artikel haben wir die Caffeine-Caching-Bibliothek für Java kennengelernt. Wir haben gesehen, wie Sie einen Cache konfigurieren und füllen sowie eine geeignete Ablauf- oder Aktualisierungsrichtlinie auswählen, die unseren Anforderungen entspricht.

Der hier gezeigte Quellcode istover on Github verfügbar.