Cache de goyave

Cache de goyave

1. Vue d'ensemble

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

Enfin, nous verrons comment utiliser les notifications de suppression que le cache peut envoyer.

2. Comment utiliser le cache de goyave

Commençons par un exemple simple: mettons en cache la forme majuscule des instancesString.

Tout d'abord, nous allons créer leCacheLoader - utilisé pour calculer la valeur stockée dans le cache. À partir de là, nous utiliserons lesCacheBuilder pratiques 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());
}

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

Notez également que nous utilisons l’opérationgetUnchecked() - elle 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 de suppression des valeurs du cache - en utilisant différents critères.

3.1. Expulsion par taille

Nous pouvonslimit the size of our cache en utilisantmaximumSize(). Si la mémoire 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 égalementlimit the cache size en utilisant une fonction de poids personnalisée. Dans le code suivant, nous utilisons leslength comme fonction de pondération 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 plusieurs enregistrements pour laisser la place à un nouvel enregistrement volumineux.

3.3. Expulsion par temps

Outre l'utilisation de la taille pour supprimer d'anciens enregistrements, nous pouvons utiliser le temps. Dans l'exemple suivant, nous personnalisons notre cache enremove records that have been idle for 2ms:

@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"));
}

On peut aussievict records based on their total live time. 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

Voyons ensuite 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 et les valeurs de cache 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 utilisantweakKeys() 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 douces

Nous pouvons autoriser garbage collector à collecter nos valeurs mises en cache en utilisantsoftValues() 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’utilisermaximumSize().

6. Gérer les valeursnull

Voyons maintenant comment gérer les valeursnull du cache. Par défaut,Guava Cache lancera des exceptions si vous essayez de charger une valeur denull - car cela n'a aucun sens de mettre en cache unnull.

Mais si la valeur denull signifie quelque chose dans votre code, vous pouvez faire bon usage de la classeOptional 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. Rafraîchir le cache

Voyons ensuite comment actualiser nos valeurs de cache. Nous pouvons actualiser notre cache automatiquement en utilisantrefreshAfterWrite().

Dans l'exemple suivant, le cache est actualisé automatiquement toutes les minutes:

@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);
}

Remarque: vous pouvez actualiser manuellement un enregistrement spécifique à l'aide derefresh(key).

8. Précharger le cache

Nous pouvons insérer plusieurs enregistrements dans notre cache en utilisant la méthodeputAll(). Dans l'exemple suivant, nous ajoutons plusieurs enregistrements dans notre cache à l'aide d'unMap:

@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. RetraitNotification

Parfois, vous devez effectuer certaines actions lorsqu'un enregistrement est supprimé du cache; alors, parlons deRemovalNotification.

Nous pouvons enregistrer unRemovalListener pour recevoir des notifications de suppression d'un enregistrement. Nous avons également accès à la cause de la suppression - via la méthodegetCause().

Dans l'exemple suivant, unRemovalNotification est reçu lorsque le quatrième élément dans le cache en raison de sa taille:

@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. Remarques

Enfin, voici quelques notes rapides supplémentaires sur la mise en œuvre du cache Guava:

  • il est thread-safe

  • vous pouvez insérer des valeurs manuellement dans le cache en utilisantput(key,value)

  • vous pouvez mesurer les performances de votre cache en utilisantCacheStats (hitRate(),missRate(), ..)

11. Conclusion

Dans ce didacticiel, nous avons parcouru de nombreux cas d'utilisation de Guava Cache - de la simple utilisation à l'éviction d'éléments, l'actualisation et le préchargement du cache et les notifications de suppression.