logo le blog invivoo blanc

Caffeine Cache

14 février 2023 | Java | 0 comments

Dans cet article, vous verrez comment optimiser les performances de votre application web et améliorer l’expérience utilisateur avec Caffeine Cache, une solution de cache rapide et fiable.

Introduction

Le cache désigne un système de mémoire intermédiaire numérique qui a pour but de réduire le temps d’accès aux données utilisées souvent.

Quand nous demandons une ressource pour une première fois, nous la récupérons depuis sa source originale et on stocke une copie en cache.

À partir du moment où la ressource est récupérée, nous ne retournerons plus que la valeur en cache. Pour éviter que le cache ne soit trop en décalage avec des mises à jour de la ressource réelle, il existe des mécanismes de purge du cache, ainsi qu’un système d’expiration.

1.   Dépendance

Pour utiliser le cache caffeine, Nous devons rajouter cette dépendance dans le pom.xml.

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.1</version>
</dependency>

2.   Chargement du cache

Il existe trois stratégies pour stocker les données dans le cache Caffeine :

  • Chargement manuel
  • Chargement synchrone
  • Chargement asynchrone

2.1 Chargement manuel

Le cache peut être initialisé manuellement. Nous pouvons mettre des données en cache et les récupérer plus tard. 

1. Initialisation du cache

Cache<String, Swap> cache = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .maximumSize(200)
        .build();

2. Chargement des données en cache

Nous pouvons utiliser la méthode « put » pour charger manuellement les données en cache.

Swap swap = Swap.builder().id("Swap1").contrat(Arrays.asList(Contrat.builder().contratId(1).build())).build();

Swap swapInCache = cache.getIfPresent(swap.getId());
Assert.assertNull(swapInCache);
System.out.println ("Swap In cache "+swapInCache);

Output
------
Swap In cache null

3. Accès au cache et récupération des données

Nous pouvons accéder au cache et récupérer les données en utilisant la méthode getIfPresent. Cette méthode retourne null si la valeur de la clé n’est pas présente dans le cache.

cache.put(swap.getId(), swap);
swapInCache = cache.getIfPresent(swap.getId());

Assert.assertNotNull(swapInCache);
System.out.println ("Swap In cache "+swapInCache);

Output:
-------
Swap In cache Swap (id=Swap1, contrat=[Contrat(contratId=1)])

Nous pouvons également obtenir la valeur en utilisant la méthode « get », qui prend une Function avec une clé comme argument. Cette fonction servira à fournir la valeur par défaut si la clé n’est pas présente dans le cache, qui serait insérée dans le cache après le calcul.

swapObj = cache
  .get(key, k ->DataObject.get("Swap1"));
assertNotNull(swapObj);
assertEquals("Data for SwapContrat1", 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. L’utilisation de get est préférable à getIfPresent.

4. Invalidation du cache

Nous pouvons invalider manuellement certaines valeurs dans le cache en utilisant la méthode invalidate.

cache.invalidate(swap.getId());
swapInCache = cache.getIfPresent(swap.getId());

Assert.assertNull(swapInCache);
System.out.println ("Swap In cache "+swapInCache);

Output:
--------
Swap In cache null

2.2.  Chargement synchrone

Pour le chargement synchrone, nous pouvons utiliser la méthode « build » qui prend en argument une fonction qui permet d’initialiser le cache.

1. Initialisation du cache

LoadingCache<String, Swap> cache = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build(k -> getSwapFromDatabase(k));

2. Accès au cache et récupération des données

Nous pouvons accéder au cache et récupérer les données en utilisant la méthode « get ». Cette méthode retourne la valeur de la clé dans le cache.

Swap swap = cache.get ("Swap_2");
assertNotNull(swap);
assertEquals ("Swap_2", swap.getId());

Nous pouvons également obtenir un ensemble de valeurs en utilisant la méthode « getAll ». Cette méthode prend en argument une liste de clés.

Map<String, Swap> swapMap = cache.getAll(Arrays.asList("Swap_1", "Swap_2", "Swap_3"));
assertEquals(3, swapMap.size());

Les valeurs sont récupérées en faisant appel à la fonction utilisée comme argument dans la méthode « build ». Cela permet d’utiliser le cache comme façade principale pour accéder aux valeurs.

2.3.  Chargement asynchrone

Ce type de changement est similaire au chargement synchrone mais il effectue les opérations de manière asynchrone et il renvoie un CompletableFuture contenant la valeur réelle.

1. Initialisation du cache

AsyncLoadingCache<String, Swap> cacheAsync = Caffeine.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .buildAsync(k -> getSwapFromDatabase(k));

2. Accès au cache et récupération des données

Nous pouvons accéder au cache et récupérer les données en utilisant soit la méthode «get»soit la méthode getAll. Dans le cas asynchrone, ces méthodes retournent des CompletableFuture.

String key = "Swap_5";
cacheAsync.get(key).thenAccept(swap-> {
    assertNotNull(swap);
    assertEquals(key, swap.getId());
});

cacheAsync.getAll(Arrays.asList("swap1", "swap2", "swap3"))
        .thenAccept(dataObjectMap -> assertEquals(3,dataObjectMap.size()));
 

3.   Éviction du cache

La configuration du cache pour l’éviction des valeurs se base sur trois stratégies :

  • Éviction basée sur la taille
  • Éviction basés sur le délai d’expiration
  • Éviction basé sur la référence

3.1.  Eviction basée sur la taille

Ce type d’éviction se base sur la configuration de la taille limite de cache. Si la taille limite est atteinte l’éviction du cache aura lieu.

Il y a deux façons pour calculer la taille des objets :

  • Soit nous comptons les objets dans le cache
  • Soit nous calculons le poids des objets dans le cache.

La taille initiale du cache par défaut est égale à zéro. Quand nous ajoutons une valeur, la taille augmente évidemment.

LoadingCache<String, Swap> cache = Caffeine.newBuilder()
        .maximumSize(1)
        .build(k -> getSwapFromDatabase(k));

assertEquals(0, cache.estimatedSize());
cache.get ("Swap_6");
assertEquals (1, cache.estimatedSize());

Nous pouvons ajouter une deuxième valeur dans le cache, ce qui entraîne la suppression de la première valeur :

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

Par défaut, Le cache caffeinen’effectue pas le  nettoyage et l’éviction des valeurs instantanément après l’expiration d’une valeur. Si votre cache est à haut débit, vous n’avez pas à vous soucier de la maintenance du cache pour nettoyer les valeurs expirées. Par contre, si votre cache est lu et écrit rarement, vous pouvez utiliser la méthode « cleanUp() »comme décrit ci-dessus.

Nous pouvons également configurer la taille limite du cache en se basant sur le poids comme décrit ci-dessous :

LoadingCache<String, Swap> cache = Caffeine.newBuilder()
        .maximumWeight(10)
        .weigher((k, v) -> 5)
        .build(k -> getSwapFromDatabase(k));

assertEquals(0, cache.estimatedSize());

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

cache.get("Swap_2");
assertEquals(2, cache.estimatedSize());

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

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

assertEquals(2, cache.estimatedSize());

3.2.  Eviction basée sur le délai d’expiration

Cette stratégie d’éviction est basée sur le délai d’expiration des valeurs en cache.  L’API caffeine nous propose trois types d’expiration :

  • Expiration après accès au cache : les valeurs en cache expirent après une période fixe X suite au dernier accès (écriture ou lecture)
  • Expiration après l’écriture en cache : les valeurs en cache expirent après une période fixe X suite à la dernière écriture.
  • Expiration customisée : la période d’expiration est calculée pour chaque type d’accès.

La configuration du cache pour éviction après accès se base sur la méthode expireAfterAccess()

LoadingCache<String, Swap> cache = Caffeine.newBuilder()
        .expireAfterAccess(5, TimeUnit.MINUTES)
        .build(k -> getSwapFromDatabase(k));

La configuration du cache pour éviction après écriture se base sur la méthode expireAfterWrite()

cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .weakKeys()
        .weakValues()
        .build(k -> getSwapFromDatabase(k));

Pour calculer la période d’expiration pour chaque type d’accès, nous devons implémenter l’interface Expiration :

cache = Caffeine.newBuilder().expireAfter(new Expiry<String, Swap>() {

    @Override
    public long expireAfterCreate(String key, Swap value, long currentTime) {
        return value.getContrat().size() * 1000;
    }
    @Override
    public long expireAfterUpdate(String key, Swap value, long currentTime, long currentDuration) {
        return currentDuration;
    }
    @Override
    public long expireAfterRead(String key, Swap value, long currentTime, long currentDuration) {
        return currentDuration;
    }
}).build(k -> getSwapFromDatabase(k));

3.3.  Éviction basée sur la référence

Il est possible de faire appel à des références faibles pour les clefs et les valeurs ce qui transfère la gestion de l’éviction au Garbage Collector. Des références fortes peuvent aussi être utilisées pour les valeurs. Notez que les références de valeurs faibles et fortes ne sont pas supportées par le cache AsyncCache.

Caffeine.weakKeys() : cette méthode permet de stocker les clés en utilisant des références faibles. Ce type de référence rend la clé référencée éligible pour GC.

Caffeine.weakValues() : cette méthode permet de stocker les valeurs en utilisant des références faibles. Ce type de référence rend la valeur référencée éligible pour GC.

Caffeine.softValues() :  cette méthode permet de stocker les valeurs en utilisant des références fortes. Ce type de référence rend la valeur référencée non éligible pour GC.

LoadingCache<String, Swap> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .weakKeys()
        .weakValues()
        .build(k -> getSwapFromDatabase(k));

cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .softValues()
        .build(k -> getSwapFromDatabase(k));

4.   Rafraichissement du cache

Il est possible de configurer le cache pour actualiser automatiquement les valeurs après une période définie. Pour cela, nous pouvons utiliser la méthode refreshAfterWrite :

LoadingCache<User, List<Product>> cache = CacheBuilder
 .newBuilder()
 .refreshAfterWrite(2, TimeUnit.MINUTES)
 .build(new CacheLoader<User, List<Product>>() {
  @Override
  public List<Product>load(User user) throws Exception {
   return loadUserFromDatabase(user);
  }
  @Override
  public ListenableFuture<List<Product>>reload(final User user, List<Product>oldValue) throws Exception {
   return listeningExecutorService.submit(new Callable<List<Product>>() {
    public List<Product>call() throws Exception {
     return load(user);
    }
   });
  }
 });

Lors de la déclaration de notre cache, nous avons défini la propriété refreshAfterWrite. Cette méthode permet de rendre une clé éligible au refresh après la période spécifiée en paramètre (2 minutes dans notre exemple).  Attention, cela signifie que la valeur de la clé sera rafraîchie lors du prochain accès au cache, qui peut être bien au-delà des deux minutes. Dans cette situation, le premier appel d’une clé déjà en cache ayant lieu après 2 minutes retournera l’ancienne valeur et la nouvelle sera calculée pour un appel ultérieur.

5.   Statistique du cache

Il est possible d’activer les statistiques lors de la création d’un cache avec la méthode «recordStats()».
Il sera alors possible d’utiliser la méthode « Cache.stats() » pour accéder à un nombre important de métriques comme le taux de hits et le nombre d’évictions et ainsi pouvoir publier ces informations en JMX très facilement.

LoadingCache<String, Swap> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .recordStats()
            .build(k -> getSwapFromDatabase(k));

cache.get("Swap1");

assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());
    System.out.println(cache.stats());

output:

CacheStats{hitCount=1, missCount=1, loadSuccessCount=1, loadFailureCount=0, totalLoadTime=2101500, evictionCount=0, evictionWeight=0}

Conclusion Caffeine Cache

Le cache Caffeine est un système de mémoire intermédiaire numérique qui permet de stocker les données en cache pour accélérer leur accès ultérieur. Cet article a présenté les différentes méthodes de chargement de données dans le cache Caffeine, à savoir le chargement manuel, synchrone et asynchrone. Nous avons vu comment initialiser le cache, charger les données, y accéder et récupérer les données.

En outre, l’article a montré comment invalider manuellement les valeurs dans le cache en utilisant la méthode invalidate.

Le cache Caffeine offre un ensemble de fonctionnalités utiles pour améliorer les performances de l’application, et est donc une solution de choix pour la gestion du cache en Java !