Cache de goyave

1. Vue d'ensemble

Dans ce didacticiel, nous examinerons l' implémentation de Guava Cache - utilisation de base, politiques d'éviction, actualisation du cache et quelques opérations groupées intéressantes.

Enfin, nous examinerons l'utilisation des notifications de suppression que le cache est capable d'envoyer.

2. Comment utiliser Guava Cache

Commençons par un exemple simple - mettons en cache la forme majuscule des instances String .

Tout d'abord, nous allons créer le CacheLoader - utilisé pour calculer la valeur stockée dans le cache. À partir de là, nous utiliserons le pratique CacheBuilder pour créer notre cache en utilisant les spécifications données:

@Test public void whenCacheMiss_thenValueIsComputed() { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder().build(loader); assertEquals(0, cache.size()); assertEquals("HELLO", cache.getUnchecked("hello")); assertEquals(1, cache.size()); }

Remarquez qu'il n'y a pas de valeur dans le cache pour notre clé «hello» - et ainsi la valeur est calculée et mise en cache.

Notez également que nous utilisons l' opération getUnchecked () - cela calcule et charge la valeur dans le cache si elle n'existe pas déjà.

3. Politiques d'expulsion

Chaque cache doit supprimer des valeurs à un moment donné. Discutons du mécanisme d'éviction des valeurs du cache - en utilisant différents critères.

3.1. Expulsion par taille

Nous pouvons limiter la taille de notre cache en utilisant maximumSize () . Si le cache atteint la limite, les éléments les plus anciens seront expulsés.

Dans le code suivant, nous limitons la taille du cache à 3 enregistrements:

@Test public void whenCacheReachMaxSize_thenEviction() { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder().maximumSize(3).build(loader); cache.getUnchecked("first"); cache.getUnchecked("second"); cache.getUnchecked("third"); cache.getUnchecked("forth"); assertEquals(3, cache.size()); assertNull(cache.getIfPresent("first")); assertEquals("FORTH", cache.getIfPresent("forth")); }

3.2. Expulsion au poids

Nous pouvons également limiter la taille du cache à l' aide d'une fonction de pondération personnalisée. Dans le code suivant, nous utilisons la longueur comme fonction de poids personnalisée:

@Test public void whenCacheReachMaxWeight_thenEviction() { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; Weigher weighByLength; weighByLength = new Weigher() { @Override public int weigh(String key, String value) { return value.length(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder() .maximumWeight(16) .weigher(weighByLength) .build(loader); cache.getUnchecked("first"); cache.getUnchecked("second"); cache.getUnchecked("third"); cache.getUnchecked("last"); assertEquals(3, cache.size()); assertNull(cache.getIfPresent("first")); assertEquals("LAST", cache.getIfPresent("last")); }

Remarque: le cache peut supprimer plus d'un enregistrement pour laisser de la place pour un nouveau grand.

3.3. Expulsion par temps

En plus d'utiliser la taille pour expulser d'anciens enregistrements, nous pouvons utiliser le temps. Dans l'exemple suivant, nous personnalisons notre cache pour supprimer les enregistrements inactifs depuis 2 ms :

@Test public void whenEntryIdle_thenEviction() throws InterruptedException { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder() .expireAfterAccess(2,TimeUnit.MILLISECONDS) .build(loader); cache.getUnchecked("hello"); assertEquals(1, cache.size()); cache.getUnchecked("hello"); Thread.sleep(300); cache.getUnchecked("test"); assertEquals(1, cache.size()); assertNull(cache.getIfPresent("hello")); }

Nous pouvons également expulser des enregistrements en fonction de leur durée de vie totale . Dans l'exemple suivant, le cache supprimera les enregistrements après 2 ms de stockage:

@Test public void whenEntryLiveTimeExpire_thenEviction() throws InterruptedException { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder() .expireAfterWrite(2,TimeUnit.MILLISECONDS) .build(loader); cache.getUnchecked("hello"); assertEquals(1, cache.size()); Thread.sleep(300); cache.getUnchecked("test"); assertEquals(1, cache.size()); assertNull(cache.getIfPresent("hello")); }

4. Clés faibles

Ensuite, voyons comment faire en sorte que nos clés de cache aient des références faibles - permettant au garbage collector de collecter des clés de cache qui ne sont pas référencées ailleurs.

Par défaut, les clés de cache et les valeurs ont des références fortes mais nous pouvons faire en sorte que notre cache stocke les clés en utilisant des références faibles en utilisant lowKeys () comme dans l'exemple suivant:

@Test public void whenWeakKeyHasNoRef_thenRemoveFromCache() { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder().weakKeys().build(loader); }

5. Valeurs souples

Nous pouvons permettre au garbage collector de collecter nos valeurs mises en cache en utilisant softValues ​​() comme dans l'exemple suivant:

@Test public void whenSoftValue_thenRemoveFromCache() { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder().softValues().build(loader); }

Remarque: De nombreuses références logicielles peuvent affecter les performances du système - il est préférable d'utiliser maximumSize () .

6. Gérer les valeurs nulles

Voyons maintenant comment gérer les valeurs nulles du cache . Par défaut, Guava Cache lèvera des exceptions si vous essayez de charger une valeur nulle - car cela n'a aucun sens de mettre en cache une valeur nulle .

Mais si la valeur nulle signifie quelque chose dans votre code, vous pouvez faire bon usage de la classe Optional comme dans l'exemple suivant:

@Test public void whenNullValue_thenOptional() { CacheLoader
    
      loader; loader = new CacheLoader
     
      () { @Override public Optional load(String key) { return Optional.fromNullable(getSuffix(key)); } }; LoadingCache
      
        cache; cache = CacheBuilder.newBuilder().build(loader); assertEquals("txt", cache.getUnchecked("text.txt").get()); assertFalse(cache.getUnchecked("hello").isPresent()); } private String getSuffix(final String str) { int lastIndex = str.lastIndexOf('.'); if (lastIndex == -1) { return null; } return str.substring(lastIndex + 1); }
      
     
    

7. Actualisez le cache

Ensuite, voyons comment actualiser nos valeurs de cache.

7.1. Actualisation manuelle

Nous pouvons actualiser manuellement une seule clé à l'aide de LoadingCache.refresh (clé).

String value = loadingCache.get("key"); loadingCache.refresh("key");

Cela forcera CacheLoader à charger la nouvelle valeur de la clé.

Jusqu'à ce que la nouvelle valeur soit correctement chargée, la valeur précédente de la clé sera renvoyée par get (key) .

7.2. Actualisation automatique

Nous pouvons utiliser CacheBuilder.refreshAfterWrite (durée) pour actualiser automatiquement les valeurs mises en cache.

@Test public void whenLiveTimeEnd_thenRefresh() { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder() .refreshAfterWrite(1,TimeUnit.MINUTES) .build(loader); }

It's important to understand that refreshAfterWrite(duration) only makes a key eligible for the refresh after the specified duration. The value will actually be refreshed only when a corresponding entry is queried by get(key).

8. Preload the Cache

We can insert multiple records in our cache using putAll() method. In the following example, we add multiple records into our cache using a Map:

@Test public void whenPreloadCache_thenUsePutAll() { CacheLoader loader; loader = new CacheLoader() { @Override public String load(String key) { return key.toUpperCase(); } }; LoadingCache cache; cache = CacheBuilder.newBuilder().build(loader); Map map = new HashMap(); map.put("first", "FIRST"); map.put("second", "SECOND"); cache.putAll(map); assertEquals(2, cache.size()); }

9. RemovalNotification

Sometimes, you need to take some actions when a record is removed from the cache; so, let's discuss RemovalNotification.

We can register a RemovalListener to get notifications of a record being removed. We also have access to the cause of the removal – via the getCause() method.

In the following sample, a RemovalNotification is received when the forth element in the cache because of its size:

@Test public void whenEntryRemovedFromCache_thenNotify() { CacheLoader loader; loader = new CacheLoader() { @Override public String load(final String key) { return key.toUpperCase(); } }; RemovalListener listener; listener = new RemovalListener() { @Override public void onRemoval(RemovalNotification n){ if (n.wasEvicted()) { String cause = n.getCause().name(); assertEquals(RemovalCause.SIZE.toString(),cause); } } }; LoadingCache cache; cache = CacheBuilder.newBuilder() .maximumSize(3) .removalListener(listener) .build(loader); cache.getUnchecked("first"); cache.getUnchecked("second"); cache.getUnchecked("third"); cache.getUnchecked("last"); assertEquals(3, cache.size()); }

10. Notes

Finally, here are a few additional quick notes about the Guava cache implementation:

  • it is thread-safe
  • you can insert values manually into the cache using put(key,value)
  • you can measure your cache performance using CacheStats ( hitRate(), missRate(), ..)

11. Conclusion

Nous avons parcouru de nombreux cas d'utilisation du cache de goyave dans ce tutoriel - de la simple utilisation à l'expulsion d'éléments, en passant par l'actualisation et le préchargement du cache et les notifications de suppression.

Comme d'habitude, tous les exemples peuvent être trouvés sur GitHub.