Introduction à la caféine

1. Introduction

Dans cet article, nous allons jeter un œil à Caffeine - une bibliothèque de mise en cache haute performance pour Java .

Une différence fondamentale entre un cache et une carte est qu'un cache expulse les éléments stockés.

Une politique d'éviction décide quels objets doivent être supprimés à tout moment. Cette politique affecte directement le taux de réussite du cache - une caractéristique cruciale de la mise en cache des bibliothèques.

Caffeine utilise la politique d'expulsion de Window TinyLfu , qui fournit un taux de réussite presque optimal .

2. Dépendance

Nous devons ajouter la dépendance à la caféine à notre pom.xml :

 com.github.ben-manes.caffeine caffeine 2.5.5 

Vous pouvez trouver la dernière version de la caféine sur Maven Central.

3. Remplissage du cache

Concentrons-nous sur les trois stratégies de Caffeine pour le remplissage du cache : le chargement manuel, synchrone et asynchrone.

Tout d'abord, écrivons une classe pour les types de valeurs que nous allons stocker dans notre cache:

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. Remplissage manuel

Dans cette stratégie, nous mettons manuellement les valeurs dans le cache et les récupérons plus tard.

Initialisons notre cache:

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

Maintenant, nous pouvons obtenir une valeur du cache en utilisant la méthode getIfPresent . Cette méthode retournera null si la valeur n'est pas présente dans le cache:

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

Nous pouvons remplir le cache manuellement en utilisant la méthode put :

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

Nous pouvons également obtenir la valeur en utilisant la méthode get , qui prend une fonction avec une clé comme argument. Cette fonction sera utilisée pour fournir la valeur de repli si la clé n'est pas présente dans le cache, qui serait insérée dans le cache après le calcul:

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

La méthode get effectue le calcul de manière atomique. Cela signifie que le calcul ne sera effectué qu'une seule fois - même si plusieurs threads demandent la valeur simultanément. C'est pourquoi l' utilisation de get est préférable à getIfPresent .

Parfois, nous devons invalider manuellement certaines valeurs mises en cache :

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

3.2. Chargement synchrone

Cette méthode de chargement du cache prend une fonction, qui est utilisée pour initialiser les valeurs, similaire à la méthode get de la stratégie manuelle. Voyons comment nous pouvons utiliser cela.

Tout d'abord, nous devons initialiser notre cache:

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

Nous pouvons maintenant récupérer les valeurs en utilisant la méthode get :

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

Nous pouvons également obtenir un ensemble de valeurs en utilisant la méthode getAll :

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

Les valeurs sont extraites de la fonction d' initialisation principale sous-jacente qui a été transmise à la méthode de génération . Cela permet d'utiliser le cache comme façade principale pour accéder aux valeurs.

3.3. Chargement asynchrone

Cette stratégie fonctionne de la même manière que la précédente mais effectue des opérations de manière asynchrone et retourne un CompletableFuture contenant la valeur réelle:

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

Nous pouvons utiliser les méthodes get et getAll , de la même manière, en tenant compte du fait qu'elles retournent 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 dispose d'une API riche et utile, sur laquelle vous pouvez en savoir plus dans cet article.

4. Expulsion des valeurs

La caféine a trois stratégies d'éviction de valeur : basée sur la taille, basée sur le temps et basée sur la référence.

4.1. Expulsion basée sur la taille

Ce type d'expulsion suppose que l' expulsion se produit lorsque la limite de taille configurée du cache est dépassée . Il existe deux façons d'obtenir la taille : compter les objets dans le cache ou obtenir leur poids.

Voyons comment nous pourrions compter les objets dans le cache . Lorsque le cache est initialisé, sa taille est égale à zéro:

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

Quand on ajoute une valeur, la taille augmente évidemment:

cache.get("A"); assertEquals(1, cache.estimatedSize());

Nous pouvons ajouter la deuxième valeur au cache, ce qui conduit à la suppression de la première valeur:

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

Il est à noter que nous appelons la méthode cleanUp avant d'obtenir la taille du cache . En effet, l'expulsion du cache est exécutée de manière asynchrone, et cette méthode permet d'attendre la fin de l'expulsion .

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

Dans cet article, nous nous sommes familiarisés avec la bibliothèque de mise en cache Caffeine pour Java. Nous avons vu comment configurer et remplir un cache, ainsi que comment choisir une politique d'expiration ou d'actualisation appropriée en fonction de nos besoins.

Le code source présenté ici est disponible à l'adresse over sur Github.