Utilizzo di memcache

Questa pagina descrive come configurare e monitorare il servizio memcache per la tua applicazione utilizzando la console Google Cloud. Descrive inoltre come eseguire attività comuni utilizzando l'interfaccia JCache e come gestire le scritture simultanee utilizzando l'API memcache di App Engine di basso livello per Java. Per saperne di più su memcache, consulta la Panoramica di Memcache.

Configurazione di memcache

  1. Vai alla pagina Memcache nella console Google Cloud.
    Vai alla pagina Memcache
  2. Seleziona il livello di servizio memcache che vuoi utilizzare:

    • Condivisa (impostazione predefinita): gratuita e fornisce capacità della cache secondo il criterio del "best effort".
    • Dedicata: viene fatturata in base ai GB/ora di dimensione della cache e fornisce una capacità della cache fissa assegnata esclusivamente alla tua applicazione.

    Scopri di più sulle classi di servizio disponibili nella Panoramica di Memcache.

Utilizzo di JCache

L'SDK Java di App Engine supporta l'interfaccia JCache (JSR 107) per accedere a memcache. L'interfaccia è inclusa nel pacchetto javax.cache.

Con JCache puoi impostare e recuperare valori, controllare la scadenza dei valori dalla cache, ispezionare i contenuti della cache e ottenere statistiche sulla cache. Puoi anche utilizzare gli "ascoltatori" per aggiungere un comportamento personalizzato durante l'impostazione e l'eliminazione dei valori.

L'implementazione di App Engine tenta di implementare un sottoinsieme fedele dello standard dell'API JCache. Per ulteriori informazioni su JCache, consulta la JSR 107. Tuttavia, anziché utilizzare JCache, ti consigliamo di utilizzare l'API Memcache a basso livello per accedere a più funzionalità del servizio sottostante.

Ottenere un'istanza della cache

Utilizzi un'implementazione dell'interfaccia javax.cache.Cache per interagire con la cache. Puoi ottenere un'istanza Cache utilizzando un CacheFactory, che puoi ottenere da un metodo statico su CacheManager. Il seguente codice recupera un'istanza Cache con la configurazione predefinita:

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) {
            // ...
        }

Il metodo createCache() di CacheFactory accetta una mappa di proprietà di configurazione. Queste proprietà sono descritte di seguito. Per accettare i valori predefiniti, passa al metodo una mappa vuota.

Inserire e recuperare valori

La cache si comporta come una mappa: memorizzi chiavi e valori utilizzando il metodo put() e li recuperi utilizzando il metodo get(). Puoi utilizzare qualsiasi oggetto Serializable per la chiave o il valore.

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

Per inserire più valori, puoi chiamare il metodo putAll() con una mappa come argomento.

Per rimuovere un valore dalla cache (per eseguirne l'espulsione immediata), chiama il metodo remove() con la chiave come argomento. Per rimuovere tutti i valori dalla cache dell'applicazione, chiama il metodo clear().

Il metodo containsKey() prende una chiave e restituisce un boolean (true o false) per indicare se nella cache esiste un valore con quella chiave. Il metodo isEmpty() verifica se la cache è vuota. Il metodo size() restituisce il numero di valori attualmente nella cache.

Configurazione della scadenza

Per impostazione predefinita, tutti i valori rimangono nella cache il più a lungo possibile, fino a quando non vengono espulsi a causa di una pressione sulla memoria, rimossi esplicitamente dall'app o resi non disponibili per un altro motivo (ad esempio un'interruzione del servizio). L'app può specificare un'ora di scadenza per i valori, ovvero il periodo di tempo massimo durante il quale il valore sarà disponibile. La data e l'ora di scadenza possono essere impostate come un periodo di tempo relativo al momento in cui viene impostato il valore o come data e ora assolute.

Specifica il criterio di scadenza utilizzando le proprietà di configurazione quando crei l'istanza Cache. Tutti i valori inseriti con l'istanza utilizzano lo stesso criterio di scadenza. Ad esempio, per configurare un'istanza Cache in modo che i valori scadano un'ora (3600 secondi) dopo essere stati impostati:

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) {
            // ...
        }

Le seguenti proprietà controllano la scadenza del valore:

  • GCacheFactory.EXPIRATION_DELTA: imposta la scadenza dei valori dopo un determinato periodo di tempo rispetto al momento in cui vengono inseriti, come numero intero di secondi
  • GCacheFactory.EXPIRATION_DELTA_MILLIS: imposta la scadenza dei valori dopo un determinato periodo di tempo rispetto al momento in cui vengono inseriti, come numero intero di millisecondi
  • GCacheFactory.EXPIRATION: fa scadere i valori alla data e all'ora specificate, come java.util.Date

Configurazione del criterio impostato

Per impostazione predefinita, l'impostazione di un valore nella cache lo aggiunge se non esiste un valore con la chiave specificata e lo sostituisce se esiste un valore con la chiave specificata. Puoi configurare la cache in modo che aggiunga solo (proteggi i valori esistenti) o sostituisca solo i valori (non li aggiunge).

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

Le seguenti proprietà controllano il criterio impostato:

  • MemcacheService.SetPolicy.SET_ALWAYS: aggiungi il valore se non esiste un valore con la chiave, sostituisci un valore esistente se esiste un valore con la chiave. Questo è il valore predefinito
  • MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT: aggiungi il valore se non esiste un valore con la chiave, non fare nulla se la chiave esiste
  • MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT: non fare nulla se non esiste un valore con la chiave, sostituisci un valore esistente se esiste un valore con la chiave

Recupero delle statistiche della cache

L'app può recuperare le statistiche sul proprio utilizzo della cache. Queste statistiche sono utili per monitorare e ottimizzare il comportamento della cache. Accedi alle statistiche utilizzando un oggetto CacheStatistics, che ottieni chiamando il metodo getCacheStatistics() della cache.

Le statistiche disponibili includono il numero di hit della cache (get per chiavi esistenti), il numero di mancate hit della cache (get per chiavi non esistenti) e il numero di valori nella cache.

import javax.cache.CacheStatistics;

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

L'implementazione di App Engine non supporta il ripristino dei conteggi hit e miss, che vengono mantenuti a tempo indeterminato, ma possono essere reimpostati a causa di condizioni transitorie dei server memcache.

Monitoraggio di Memcache nella console Google Cloud

  1. Vai alla pagina Memcache nella console Google Cloud.
    Vai alla pagina Memcache
  2. Esamina i seguenti report:
    • Livello di servizio memcache: indica se la tua applicazione utilizza il livello di servizio condiviso o dedicato. Se sei un proprietario del progetto, puoi passare da una modalità all'altra. Scopri di più sui livelli di servizio.
    • Rapporto hit: mostra la percentuale di richieste di dati che sono state soddisfatte dalla cache, nonché il numero non elaborato di richieste di dati che sono state soddisfatte dalla cache.
    • Elementi nella cache.
    • Età elemento più vecchio: l'età dell'elemento più vecchio memorizzato nella cache. Tieni presente che l'età di un elemento viene reimpostata ogni volta che viene utilizzato, letto o scritto.
    • Dimensione cache totale.
  3. Puoi eseguire una delle seguenti azioni:

    • Nuova chiave: aggiungi una nuova chiave alla cache.
    • Trova una chiave: recupera una chiave esistente.
    • Svuota cache: rimuovi tutte le coppie chiave-valore dalla cache.
  4. (Solo memcache dedicata) Consulta l'elenco delle Chiavi calde.

    • Le "Chiavi calde" sono chiavi che ricevono più di 100 query al secondo (QPS) nel memcache.
    • Questo elenco include fino a 100 tasti di scelta rapida, ordinati in base alla QPS più alta.

Gestione delle scritture simultanee

Se stai aggiornando il valore di una chiave memcache che potrebbe ricevere altre richieste di scrittura simultanee, devi utilizzare i metodi memcache di basso livello putIfUntouched e getIdentifiable anziché put e get. I metodi putIfUntouched e getIdentifiable evitano le condizioni di gara consentendo a più richieste gestite contemporaneamente di aggiornare atomically il valore della stessa chiave memcache.

Lo snippet di codice seguente mostra un modo per aggiornare in sicurezza il valore di una chiave che potrebbe avere richieste di aggiornamento simultanee da altri client:

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

Un perfezionamento che puoi aggiungere a questo codice campione è impostare un limite al numero di tentativi, per evitare un blocco così lungo da far scadere il tempo di attesa della richiesta di App Engine.

Passaggi successivi