Einführung in Koffein

1. Einleitung

In diesem Artikel werfen wir einen Blick auf Caffeine - eine leistungsstarke Caching-Bibliothek für Java .

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

Eine Räumungsrichtlinie entscheidet, welche Objekte zu einem bestimmten Zeitpunkt gelöscht werden sollen . Diese Richtlinie wirkt sich direkt auf die Trefferquote des Caches aus - ein entscheidendes Merkmal beim Zwischenspeichern von Bibliotheken.

Koffein verwendet die Räumungsrichtlinie von Window TinyLfu , die eine nahezu optimale Trefferquote bietet .

2. Abhängigkeit

Wir müssen die Koffeinabhängigkeit zu unserer pom.xml hinzufügen :

 com.github.ben-manes.caffeine caffeine 2.5.5 

Sie finden die neueste Version von Koffein auf Maven Central.

3. Cache füllen

Konzentrieren wir uns auf die drei Strategien von Caffeine für die Cache-Population : manuelles, synchrones Laden 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 legen wir Werte manuell in den Cache und rufen sie später ab.

Initialisieren wir unseren Cache:

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

Jetzt können wir mit der Methode getIfPresent einen Wert aus dem Cache abrufen . Diese Methode gibt null zurück, wenn der Wert nicht im Cache vorhanden ist:

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

Wir können den Cache manuell mit der put- Methode füllen:

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

Wir können den Wert auch mit der get- Methode abrufen , die eine Funktion zusammen mit einem Schlüssel als Argument verwendet. 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 wird:

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

Die get- 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. Aus diesem Grund ist die Verwendung von get der Verwendung von getIfPresent vorzuziehen .

Manchmal müssen wir einige zwischengespeicherte Werte manuell ungültig machen :

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

3.2. Synchrones Laden

Diese Methode zum Laden des Caches benötigt eine Funktion, die zum Initialisieren von Werten verwendet wird, ähnlich der get- Methode 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 get- Methode abrufen :

DataObject dataObject = cache.get(key); assertNotNull(dataObject); assertEquals("Data for " + key, dataObject.getData());

Mit der Methode getAll können wir auch eine Reihe von Werten abrufen :

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

Die Werte werden von der darunterliegenden Back-End - Initialisierung abgerufenen Funktion , die auf die übergeben wurde build - Methode. Dadurch ist es möglich, den Cache als Hauptfassade für den Zugriff auf Werte zu verwenden.

3.3. Asynchrones Laden

Diese Strategie funktioniert genauso wie die vorherige, führt jedoch Operationen asynchron aus und gibt eine CompletableFuture zurück, die den tatsächlichen Wert enthält:

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

Wir können die Methoden get und getAll auf die gleiche Weise verwenden, wobei wir die Tatsache berücksichtigen, dass sie CompletableFuture 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, über die Sie in diesem Artikel mehr lesen können.

4. Räumung von Werten

Koffein hat drei Strategien zur Werträumung : größenbasiert, zeitbasiert und referenzbasiert.

4.1. Größenbasierte Räumung

Diese Art der Räumung setzt voraus, dass die Räumung erfolgt, wenn die konfigurierte Größenbeschränkung des Caches überschritten wird . Es gibt zwei Möglichkeiten, die Größe zu ermitteln : Objekte im Cache zählen oder ihre Gewichte ermitteln.

Mal sehen, wie wir Objekte im Cache zählen 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, was zum Entfernen des ersten Werts führt:

cache.get("B"); cache.cleanUp(); assertEquals(1, cache.estimatedSize());

Es ist erwähnenswert, dass wir die cleanUp- Methode aufrufen , bevor wir die Cache-Größe ermitteln . Dies liegt daran, dass die Cache-Räumung asynchron ausgeführt wird und diese Methode dazu beiträgt, den Abschluss der Räumung abzuwarten .

We can also pass a weigherFunctionto get the size of the cache:

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

The values are removed from the cache when the weight is over 10:

cache.get("C"); cache.cleanUp(); assertEquals(2, cache.estimatedSize());

4.2. Time-Based Eviction

This eviction strategy is based on the expiration time of the entry and has three types:

  • Expire after access — entry is expired after period is passed since the last read or write occurs
  • Expire after write — entry is expired after period is passed since the last write occurs
  • Custom policy — an expiration time is calculated for each entry individually by the Expiry implementation

Let's configure the expire-after-access strategy using the expireAfterAccess method:

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

To configure expire-after-write strategy, we use the expireAfterWrite method:

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

To initialize a custom policy, we need to implement the Expiry interface:

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. Reference-Based Eviction

We can configure our cache to allow garbage-collection of cache keys and/or values. To do this, we'd configure usage of the WeakRefence for both keys and values, and we can configure the SoftReference for garbage-collection of values only.

The WeakRefence usage allows garbage-collection of objects when there are not any strong references to the object. SoftReference allows objects to be garbage-collected based on the global Least-Recently-Used strategy of the JVM. More details about references in Java can be found here.

We should use Caffeine.weakKeys(), Caffeine.weakValues(), and Caffeine.softValues() to enable each option:

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. Refreshing

It's possible to configure the cache to refresh entries after a defined period automatically. Let's see how to do this using the refreshAfterWrite method:

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

Here we should understand a difference between expireAfter and refreshAfter. When the expired entry is requested, an execution blocks until the new value would have been calculated by the build Function.

But if the entry is eligible for the refreshing, then the cache would return an old value and asynchronously reload the value.

6. Statistics

Caffeine has a means of recording 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());

We may also pass into recordStats supplier, which creates an implementation of the StatsCounter. This object will be pushed with every statistics-related change.

7. Conclusion

In diesem Artikel haben wir die Caffeine Caching Library für Java kennengelernt. Wir haben gesehen, wie ein Cache konfiguriert und gefüllt wird und wie eine geeignete Ablauf- oder Aktualisierungsrichtlinie entsprechend unseren Anforderungen ausgewählt wird.

Der hier gezeigte Quellcode ist über Github verfügbar.