Introduction à la caféine

Introduction à la caféine

1. introduction

Dans cet article, nous allons examinerCaffeine - ahigh-performance caching library for Java.

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

Uneviction policy decides which objects should be deleted à un moment donné. Cette politiquedirectly affects the cache’s hit rate - une caractéristique cruciale de la mise en cache des bibliothèques.

La caféine utilise la politique d'expulsion deWindow TinyLfu, qui fournit unnear-optimal hit rate.

2. Dépendance

Nous devons ajouter la dépendancecaffeine à nospom.xml:


    com.github.ben-manes.caffeine
    caffeine
    2.5.5

Vous pouvez trouver la dernière version decaffeineon Maven Central.

3. Remplir le cache

Concentrons-nous sur lesthree strategies for cache population de Caffeine: chargement manuel, synchrone et asynchrone.

Tout d'abord, écrivons une classe pour les types de valeurs que nous stockerons 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 des 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,we can get some value from the cache using the getIfPresent method. Cette méthode retourneranull si la valeur n'est pas présente dans le cache:

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

assertNull(dataObject);

Nous pouvonspopulate the cache manuellement en utilisant la méthodeput:

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

assertNotNull(dataObject);

We can also get the value using the get method, qui prend unFunction 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, ce qui serait inséré 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éthodeget 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 pourquoiusing get is preferable to getIfPresent.

Parfois, nous avons besoin deinvalidate some cached values manuellement:

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

assertNull(dataObject);

3.2. Chargement synchrone

Cette méthode de chargement du cache prend unFunction, qui est utilisé pour l'initialisation des valeurs, similaire à la méthodeget 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éthodeget:

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éthodegetAll:

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

assertEquals(3, dataObjectMap.size());

Les valeurs sont extraites de l'initialisation back-end sous-jacenteFunction qui a été transmise à la méthodebuild. This makes it possible to use the cache as the main facade for accessing values.

3.3. Chargement asynchrone

Cette stratégieworks the same as the previous but performs operations asynchronously and returns a CompletableFuture tient la valeur réelle:

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

On peutuse the get and getAll methods, de la même manière, en tenant compte du fait qu'ils retournentCompletableFuture:

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 possède une API riche et utile, que vous pouvez en savoir plus surin this article.

4. Expulsion des valeurs

La caféine athree strategies for value eviction: basé sur la taille, basé sur le temps et basé sur la référence.

4.1. Expulsion basée sur la taille

Ce type d'expulsion suppose queeviction occurs when the configured size limit of the cache is exceeded. Il y atwo ways of getting the size - comptant des objets dans le cache, ou obtenant leur poids.

Voyons comment nous pourrionscount objects in the 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());

Lorsque nous ajoutons 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 convient de mentionner que nouscall the cleanUp method before getting the cache size. Cela est dû au fait que l'expulsion du cache est exécutée de manière asynchrone, et cette méthodehelps to await the completion of the eviction.

On peut aussipass a weigherFunction pour obtenir la taille du 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());

Les valeurs sont supprimées du cache lorsque le poids est supérieur à 10:

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

assertEquals(2, cache.estimatedSize());

4.2. Expulsion en fonction du temps

Cette stratégie d'expulsion estbased on the expiration time of the entry et comporte trois types:

  • Expire after access - l'entrée est expirée après la période écoulée depuis la dernière lecture ou écriture

  • Expire after write - l'entrée est expirée après la période écoulée depuis la dernière écriture

  • Custom policy - un délai d'expiration est calculé pour chaque entrée individuellement par l'implémentation deExpiry

Configurons la stratégie d'expiration après accès à l'aide de la méthodeexpireAfterAccess:

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

Pour configurer la stratégie d'expiration après écriture, nous utilisons la méthodeexpireAfterWrite:

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

Pour initialiser une stratégie personnalisée, nous devons implémenter l'interfaceExpiry:

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. Expulsion basée sur des références

Nous pouvons configurer notre cache pour autorisergarbage-collection of cache keys and/or values. Pour ce faire, nous allons configurer l'utilisation desWeakRefence pour les clés et les valeurs, et nous pouvons configurer lesSoftReference pour le garbage collection des valeurs uniquement.

L'utilisation deWeakRefence autorise le garbage collection d'objets lorsqu'il n'y a pas de références fortes à l'objet. SoftReference permet aux objets d'être récupérés sur la base de la stratégie globale des moins récents utilisés de la JVM. Plus de détails sur les références en Java peuvent être trouvéshere.

Nous devrions utiliserCaffeine.weakKeys(),Caffeine.weakValues(), etCaffeine.softValues() pour activer chaque 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. Rafraîchissant

Il est possible de configurer le cache pour actualiser automatiquement les entrées après une période définie. Voyons comment procéder en utilisant la méthoderefreshAfterWrite:

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

Ici, nous devons comprendre undifference between expireAfter and refreshAfter. Lorsque l'entrée expirée est demandée, une exécution se bloque jusqu'à ce que la nouvelle valeur ait été calculée par le buildFunction.

Mais si l'entrée est éligible pour l'actualisation, alors le cache renverra une ancienne valeur etasynchronously reload the value.

6. Statistiques

La caféine a une moyenne derecording 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());

Nous pouvons également passer au fournisseurrecordStats, ce qui crée une implémentation desStatsCounter.. Cet objet sera poussé à chaque changement lié aux statistiques.

7. Conclusion

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

Le code source affiché ici est disponibleover on Github.