Utiliser Memcache

Cette page explique comment configurer et surveiller le service Memcache pour votre application à l'aide de la console Google Cloud. Elle explique également comment effectuer des tâches courantes à l'aide de l'interface JCache et comment gérer des écritures simultanées à l'aide de l'API App Engine memcache de bas niveau pour Java. Pour en savoir plus sur Memcache, consultez la page Présentation de Memcache.

Configurer Memcache

  1. Accédez à la page "Memcache" de Google Cloud Console.
    Accéder à la page "Memcache"
  2. Sélectionnez le niveau de service Memcache que vous souhaitez utiliser.

    • Partagé : (valeur par défaut) niveau de service gratuit qui attribue la capacité de cache de la manière la plus optimale possible.
    • Dédié : niveau de service facturé en Go/heure de taille de cache utilisé. Il fournit une capacité de cache fixe qui est attribuée exclusivement à votre application.

    Pour en savoir plus sur les classes de service disponibles, consultez la page Présentation de Memcache.

Utilisation de JCache

Le SDK Java d'App Engine est compatible avec l'interface d'accès à memcache JCache (JSR 107). L'interface javax.cache est incluse dans le package .

Avec JCache, vous pouvez définir et extraire des valeurs, contrôler l'expiration des valeurs et leur suppression de la mémoire cache, inspecter le contenu du cache et obtenir des statistiques concernant ce dernier. Vous pouvez également utiliser des "écouteurs" pour personnaliser le comportement à la définition et à la suppression de valeurs.

La mise en œuvre d'App Engine tente d'implémenter un sous-ensemble fidèle du standard d'API JCache. (Pour plus d'informations sur JCache, consultez la norme JSR 107.) Cependant, au lieu d'utiliser JCache, vous pouvez envisager d'utiliser l'API Memcache de bas niveau afin d'avoir accès à davantage de fonctionnalités du service sous-jacent.

Obtenir une instance de cache

Vous utilisez une implémentation de l'interface javax.cache.Cache pour interagir avec le cache. Vous obtenez une instance de cache à l'aide d'un objet CacheFactory lui-même issu d'une méthode statique du CacheManager. Le code suivant permet d'obtenir une instance de cache avec la configuration par défaut :

import java.util.Collections;
import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.CacheFactory;
import javax.cache.CacheManager;

// ...
        Cache cache;
        try {
            CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory();
            cache = cacheFactory.createCache(Collections.emptyMap());
        } catch (CacheException e) {
            // ...
        }

La méthode createCache() de CacheFactory utilise un élément Map des propriétés de configuration. Ces propriétés sont présentées ci-dessous. Pour accepter les valeurs par défaut, attribuez à la méthode un élément Map vide.

Insérer et récupérer des valeurs

Le cache se comporte comme un élément Map : vous insérez des clés et des valeurs à l'aide de la méthode put(), puis extrayez des valeurs à l'aide de la méthode get(). Vous pouvez utiliser n'importe quel objet sérialisable pour la clé ou la valeur.

        String key;      // ...
        byte[] value;    // ...

        // Put the value into the cache.
        cache.put(key, value);

        // Get the value from the cache.
        value = (byte[]) cache.get(key);

Pour insérer plusieurs valeurs, vous pouvez appeler la méthode putAll() en utilisant un élément Map comme argument.

Pour supprimer une valeur du cache (afin de l'exclure immédiatement), appelez la méthode remove() en utilisant la clé comme argument. Pour supprimer toutes les valeurs du cache de l'application, appelez la méthode clear().

La méthode containsKey() utilise une clé et renvoie un objet de type boolean (true ou false) pour indiquer si une valeur associée à cette clé existe dans le cache. La méthode isEmpty() vérifie si le cache est vide. La méthode size() renvoie le nombre de valeurs actuellement présentes dans le cache.

Configurer l'expiration

Par défaut, toutes les valeurs restent dans le cache le plus longtemps possible, c'est-à-dire jusqu'à ce qu'elles soient éliminées suite à la saturation de la mémoire, supprimées explicitement par l'application ou rendues indisponibles pour une autre raison (telle qu'une panne). L'application peut spécifier un délai d'expiration pour les valeurs, c'est-à-dire une durée maximale pendant laquelle la valeur sera disponible. Le délai d'expiration peut être paramétré comme une durée relative qui dépend du moment où la valeur a été définie ou comme un horodatage absolu.

Vous pouvez spécifier la règle d'expiration à l'aide des propriétés de configuration lors de la création de l'instance de cache. Toutes les valeurs insérées avec cette instance utilisent la même règle d'expiration. L'exemple suivant permet de configurer une instance de cache afin que les valeurs expirent une heure (3 600 secondes) après qu'elles ont été définies :

import java.util.HashMap;
import java.util.Map;
import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.CacheFactory;
import javax.cache.CacheManager;
import javax.concurrent.TimeUnit;
import com.google.appengine.api.memcache.jsr107cache.GCacheFactory;

// ...
        Cache cache;
        try {
            CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory();
            Map<Object, Object> properties = new HashMap<>();
            properties.put(GCacheFactory.EXPIRATION_DELTA, TimeUnit.HOURS.toSeconds(1));
            cache = cacheFactory.createCache(properties);
        } catch (CacheException e) {
            // ...
        }

Les propriétés suivantes contrôlent l'expiration des valeurs :

  • GCacheFactory.EXPIRATION_DELTA : les valeurs expirent au terme du délai (en nombre entier de secondes) spécifié par rapport au moment où elles ont été insérées.
  • GCacheFactory.EXPIRATION_DELTA_MILLIS : les valeurs expirent au terme du délai (en nombre entier de millisecondes) spécifié par rapport au moment où elles ont été insérées.
  • GCacheFactory.EXPIRATION : les valeurs expirent aux date et heure indiquées sous la forme d'un objet java.util.Date.

Configurer la règle de définition

Par défaut, la définition d'une valeur dans le cache a pour effet de l'ajouter si aucune valeur n'est associée à la clé spécifiée et de remplacer la valeur existante si une valeur correspond déjà à la clé spécifiée. Vous pouvez configurer le cache afin que les valeurs soient uniquement ajoutées (protection des valeurs existantes) ou remplacées (pas d'ajout).

import java.util.HashMap;
import java.util.Map;
import com.google.appengine.api.memcache.MemcacheService;

// ...
        Map<Object, Object> properties = new HashMap<>();
        properties.put(MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT, true);

Les propriétés suivantes contrôlent la règle de définition :

  • MemcacheService.SetPolicy.SET_ALWAYS : ajoute la valeur si aucune valeur n'est associée à la clé ou remplace la valeur existante si une valeur correspond déjà à la clé. Il s'agit du paramètre par défaut.
  • MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT : ajoute la valeur si aucune valeur n'est associée à la clé et n'a aucun effet si la clé existe.
  • MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT : n'a aucun effet si aucune valeur n'est associée à la clé et remplace la valeur existante s'il existe déjà une valeur associée à la clé.

Récupérer les statistiques relatives au cache

L'application peut récupérer les statistiques relatives à sa propre utilisation du cache. Ces statistiques sont utiles pour contrôler et ajuster le comportement du cache. Vous pouvez accéder aux statistiques à l'aide d'un objet CacheStatistics que vous obtenez en appelant la méthode getCacheStatistics() du cache.

Les statistiques disponibles incluent le nombre d'opérations réussies en mémoire cache (opérations Get exécutées pour les clés qui existent), le nombre d'échecs d'accès au cache (opérations Get exécutées pour les clés qui n'existent pas) et le nombre de valeurs présentes dans le cache.

import javax.cache.CacheStatistics;

        CacheStatistics stats = cache.getCacheStatistics();
        int hits = stats.getCacheHits();
        int misses = stats.getCacheMisses();

La mise en œuvre d'App Engine ne permet pas de réinitialiser les décomptes hit et miss. Ceux-ci sont maintenus indéfiniment, mais peuvent être redéfinis suite à des conditions transitoires affectant les serveurs Memcache.

Surveiller Memcache dans Google Cloud Console

  1. Accédez à la page "Memcache" de Google Cloud Console.
    Accéder à la page "Memcache"
  2. Consultez les rapports ci-dessous :
    • Niveau de service Memcache : indique si l'application utilise le niveau de service partagé ou dédié. Si vous êtes propriétaire du projet, vous pouvez basculer de l'un à l'autre. En savoir plus sur les niveaux de service
    • Taux d'accès : affiche le pourcentage de réponses à des requêtes de données diffusé à partir du cache, ainsi que le nombre brut de réponses diffusées à partir du cache.
    • Éléments en cache.
    • Âge du plus ancien élément : âge de l'élément en cache le plus ancien. Notez que l'âge d'un élément est réinitialisé chaque fois que ce dernier est utilisé, qu'il soit lu ou écrit.
    • Taille totale du cache.
  3. Vous pouvez effectuer l'une des actions suivantes :

    • Nouvelle clé : ajoute une clé au cache.
    • Trouver une clé : récupère une clé existante.
    • Vider la mémoire cache : supprime toutes les paires clé/valeur du cache.
  4. (Service Memcache dédié uniquement) Parcourez la liste des Clés fréquemment utilisées.

    • Les clés de cette liste reçoivent plus de 100 requêtes par seconde (RPS) dans Memcache.
    • Cette liste peut comporter jusqu'à 100 clés, triées par ordre décroissant en fonction du nombre de RPS reçues.

Traiter les écritures simultanées

Si vous mettez à jour la valeur d'une clé memcache susceptible de faire l'objet d'autres requêtes d'écriture simultanées, vous devez utiliser les méthodes memcache de bas niveau putIfUntouched et getIdentifiable au lieu de put et get. Les méthodes putIfUntouched et getIdentifiable évitent les conditions de concurrence en autorisant le traitement simultané, de manière atomique, de plusieurs requêtes de mise à jour sur la même clé memcache.

L'extrait de code ci-dessous présente une manière de mettre à jour en toute sécurité la valeur d'une clé susceptible de faire l'objet de requêtes de mise à jour simultanées en provenance d'autres clients :

@SuppressWarnings("serial")
public class MemcacheConcurrentServlet extends HttpServlet {

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException,
      ServletException {
    String path = req.getRequestURI();
    if (path.startsWith("/favicon.ico")) {
      return; // ignore the request for favicon.ico
    }

    String key = "count-concurrent";
    // Using the synchronous cache.
    MemcacheService syncCache = MemcacheServiceFactory.getMemcacheService();

    // Write this value to cache using getIdentifiable and putIfUntouched.
    for (long delayMs = 1; delayMs < 1000; delayMs *= 2) {
      IdentifiableValue oldValue = syncCache.getIdentifiable(key);
      byte[] newValue = oldValue == null
          ? BigInteger.valueOf(0).toByteArray()
              : increment((byte[]) oldValue.getValue()); // newValue depends on old value
      resp.setContentType("text/plain");
      resp.getWriter().print("Value is " + new BigInteger(newValue).intValue() + "\n");
      if (oldValue == null) {
        // Key doesn't exist. We can safely put it in cache.
        syncCache.put(key, newValue);
        break;
      } else if (syncCache.putIfUntouched(key, oldValue, newValue)) {
        // newValue has been successfully put into cache.
        break;
      } else {
        // Some other client changed the value since oldValue was retrieved.
        // Wait a while before trying again, waiting longer on successive loops.
        try {
          Thread.sleep(delayMs);
        } catch (InterruptedException e) {
          throw new ServletException("Error when sleeping", e);
        }
      }
    }
  }

  /**
   * Increments an integer stored as a byte array by one.
   * @param oldValue a byte array with the old value
   * @return         a byte array as the old value increased by one
   */
  private byte[] increment(byte[] oldValue) {
    long val = new BigInteger(oldValue).intValue();
    val++;
    return BigInteger.valueOf(val).toByteArray();
  }
}

Vous pouvez affiner cet exemple de code en limitant le nombre de tentatives, afin d'éviter un blocage si long qu'il conduirait votre requête App Engine à expirer.

Étapes suivantes