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 basso livello di App Engine per Java. Per saperne di più su Memcache, leggi 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:

    • Condiviso (impostazione predefinita): gratuito e fornisce capacità della cache al meglio delle possibilità.
    • Dedicato: fatturato in base alle 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 l'accesso a memcache. L'interfaccia è inclusa nel pacchetto javax.cache.

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

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

Recupero di un'istanza di cache

Utilizzi un'implementazione dell'interfaccia javax.cache.Cache per interagire con la cache. Ottieni un'istanza Cache utilizzando un CacheFactory, che ottieni da un metodo statico su CacheManager. Il seguente codice recupera un'istanza di 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 delle proprietà di configurazione. Queste proprietà sono descritte di seguito. Per accettare i valori predefiniti, fornisci al metodo una mappa vuota.

Inserimento e recupero dei valori

La cache si comporta come una mappa: memorizzi le chiavi e i valori utilizzando il metodo put() e recuperi i valori utilizzando il metodo get(). Puoi utilizzare qualsiasi oggetto serializzabile 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 eliminarlo immediatamente), chiama il metodo remove() con la chiave come argomento. Per rimuovere ogni valore dalla cache per l'applicazione, chiama il metodo clear().

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

Configurazione della scadenza

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

Specifichi la policy di scadenza utilizzando le proprietà di configurazione quando crei l'istanza della cache. Tutti i valori inseriti con questa istanza utilizzano gli stessi criteri di scadenza. Ad esempio, per configurare un'istanza di 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 dei valori:

  • GCacheFactory.EXPIRATION_DELTA: i valori scadono dopo il periodo di tempo specificato rispetto al momento in cui vengono inseriti, come numero intero di secondi
  • GCacheFactory.EXPIRATION_DELTA_MILLIS: i valori scadono dopo il periodo di tempo specificato rispetto al momento in cui vengono inseriti, come numero intero di millisecondi
  • GCacheFactory.EXPIRATION: i valori scadono alla data e all'ora specificate, come java.util.Date

Configurazione del criterio impostato

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

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 la policy impostata:

  • MemcacheService.SetPolicy.SET_ALWAYS: aggiungi il valore se non esiste alcun valore con la chiave, sostituisci un valore esistente se esiste un valore con la chiave; questa è l'impostazione predefinita
  • MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT: aggiungi il valore se non esiste alcun valore con la chiave, non fare nulla se la chiave esiste
  • MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT: non fare nulla se non esiste alcun valore con la chiave, sostituisci un valore esistente se esiste un valore con la chiave

Recupero delle statistiche della cache

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

Le statistiche disponibili includono il numero di hit della cache (recuperi per chiavi esistenti), il numero di mancati hit della cache (recuperi per chiavi inesistenti) 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 la reimpostazione dei conteggi hit e miss, che vengono mantenuti a tempo indeterminato, ma potrebbero 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 il 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 gestite dalla cache, nonché il numero grezzo di richieste di dati gestite dalla cache.
    • Elementi nella cache.
    • Età elemento più vecchio: l'età dell'elemento memorizzato nella cache meno recente. 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 dedicato) Esamina l'elenco dei tasti di scelta rapida.

    • Le "hot key" sono chiavi che ricevono più di 100 query al secondo (QPS) nella memcache.
    • Questo elenco include fino a 100 tasti di scelta rapida, ordinati in base al valore QPS più alto.

Gestione delle scritture simultanee

Se aggiorni 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 race condition consentendo a più richieste gestite contemporaneamente di aggiornare il valore della stessa chiave memcache in modo atomico.

Lo snippet di codice riportato di seguito mostra un modo per aggiornare in modo sicuro 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 potresti aggiungere a questo codice campione è impostare un limite al numero di tentativi, per evitare il blocco per un periodo di tempo così lungo che la richiesta di App Engine scade.

Passaggi successivi