Usar Memcache

En esta página se describe cómo configurar y monitorizar el servicio de Memcache de tu aplicación mediante la consola Google Cloud . También se describe cómo realizar tareas habituales con la interfaz JCache y cómo gestionar las escrituras simultáneas con la API de memcached de App Engine de bajo nivel para Java. Para obtener más información sobre Memcache, consulta el artículo Información general sobre Memcache.

Configurar memcache

  1. Ve a la página Memcache de la Google Cloud consola.
    Ve a la página Memcache.
  2. Selecciona el nivel de servicio de memcache que quieras usar:

    • Compartida (opción predeterminada): es gratuita y proporciona capacidad de caché en la medida de lo posible.
    • Dedicado: se factura por GB-hora de tamaño de caché y proporciona una capacidad de caché fija asignada exclusivamente a tu aplicación.

    Consulta más información sobre las clases de servicio disponibles en Descripción general de Memcache.

Uso de JCache

El SDK de Java de App Engine admite la interfaz JCache (JSR 107) para acceder a Memcache. La interfaz se incluye en el paquete javax.cache.

Con JCache, puedes establecer y obtener valores, controlar cómo los valores vencen en la memoria caché, inspeccionar el contenido de la memoria caché y obtener estadísticas sobre ella. También puedes usar "listeners" para añadir un comportamiento personalizado al definir y eliminar valores.

La implementación de App Engine intenta implementar un subconjunto fiel del estándar de la API JCache. Para obtener más información sobre JCache, consulta JSR 107. Sin embargo, en lugar de usar JCache, puedes usar la API Memcache de nivel inferior para acceder a más funciones del servicio subyacente.

Obtener una instancia de caché

Utilizas una implementación de la interfaz javax.cache.Cache para interactuar con la caché. Puedes obtener una instancia Cache mediante una instancia CacheFactory, que se obtiene de un método estático en CacheManager. El siguiente código obtiene una instancia Cache con la configuración predeterminada:

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

El método createCache() de CacheFactory usa un mapa de propiedades de configuración. A continuación se tratan estas propiedades. Para aceptar los valores predeterminados, adjudica al método una asignación vacía.

Insertar y obtener valores

La caché se comporta como un mapa: almacenas claves y valores con el método put() y obtienes valores con el método get(). Puedes utilizar cualquier objeto serializable para la clave o para el valor.

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

Para introducir varios valores, puede llamar al método putAll() con un mapa como argumento.

Para quitar un valor de la caché (es decir, para expulsarlo inmediatamente), llama al método remove() con la clave como argumento. Para eliminar todos los valores de la caché de la aplicación, llama al método clear().

El método containsKey() toma una clave y devuelve un boolean (true o false) para indicar si existe un valor con esa clave en la caché. El método isEmpty() comprueba si la caché está vacía. El método size() devuelve el número de valores que hay en la caché.

Configurar la caducidad

De forma predeterminada, todos los valores permanecen en la memoria caché el mayor tiempo posible, hasta que se expulsan por presión de la memoria, se eliminan de forma explícita por la aplicación o dejan de estar disponibles por otra razón (como, por ejemplo, una interrupción del servicio). La aplicación puede especificar un tiempo de vencimiento para los valores, es decir, el tiempo máximo durante el que estará disponible el valor. El tiempo de vencimiento se puede establecer como una cantidad de tiempo relacionada con el momento en el que se estableció el valor o como una fecha y hora absolutas.

Puedes especificar la política de vencimiento a través de las propiedades de configuración al crear la instancia Cache. Todos los valores establecidos con esta instancia utilizan la misma política de vencimiento. Por ejemplo, para configurar una instancia Cache cuyos valores venzan una hora (3.600 segundos) después de haberse establecido:

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

Las siguientes propiedades controlan el vencimiento de los valores:

  • GCacheFactory.EXPIRATION_DELTA: los valores caducan la cantidad de tiempo indicada en relación con el momento en que se insertan, como un número entero de segundos.
  • GCacheFactory.EXPIRATION_DELTA_MILLIS: los valores caducan el tiempo indicado en relación con el momento en que se insertan, como un número entero de milisegundos.
  • GCacheFactory.EXPIRATION: caduca los valores en la fecha y hora indicadas, como java.util.Date.

Configurar la política definida

De forma predeterminada, el establecimiento de un valor en la memoria caché añade el valor si no existe ninguno con la clave especificada y sustituye un valor si existe uno con la clave especificada. Puedes configurar la memoria caché para que los valores sólo se añadan (protegiendo así los valores existentes) o para que se sustituyan (sin añadirlos).

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

Las siguientes propiedades controlan la política establecida:

  • MemcacheService.SetPolicy.SET_ALWAYS: añade el valor si no existe ningún valor con la clave o sustituye un valor si ya existe un valor con la clave. Este es el valor predeterminado.
  • MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT: añade el valor si no existe ningún valor con la clave; no hace nada si la clave ya existe.
  • MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT: no hace nada si no existe ningún valor con la clave, sustituye un valor si existe un valor con la clave

Obtener estadísticas de la caché

La aplicación puede recuperar estadísticas sobre su propio uso de la memoria caché. Estas estadísticas son útiles para controlar y ajustar el comportamiento de la memoria caché. Para acceder a las estadísticas, usa un objeto CacheStatistics, que se obtiene llamando al método getCacheStatistics() de la caché.

Las estadísticas disponibles incluyen el número de aciertos de caché (se obtiene para las claves ya existentes), el número de errores de caché (se obtiene para las claves no existentes previamente) y el número de valores de la memoria caché.

import javax.cache.CacheStatistics;

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

La implementación de App Engine no admite el restablecimiento de los recuentos de hit y miss, que se mantienen indefinidamente, pero que pueden restablecerse debido a condiciones transitorias de los servidores de memcache.

Monitorizar memcache en la consola de Google Cloud

  1. Ve a la página Memcache de la Google Cloud consola.
    Ve a la página de Memcache
  2. Consulta los siguientes informes:
    • Nivel de servicio de Memcache: muestra si tu aplicación usa el nivel de servicio compartido o el dedicado. Si eres el propietario del proyecto, puedes cambiar entre los dos. Más información sobre los niveles de servicio
    • Ratio de aciertos: muestra el porcentaje de solicitudes de datos que se han atendido desde la caché, así como el número sin procesar de solicitudes de datos que se han atendido desde la caché.
    • Elementos de la caché.
    • Antigüedad del elemento más antiguo: la antigüedad del elemento más antiguo almacenado en caché. Ten en cuenta que la antigüedad de un elemento se restablece cada vez que se usa, ya sea para leerlo o escribirlo.
    • Tamaño total de la caché.
  3. Puedes llevar a cabo cualquiera de las siguientes acciones:

    • Nueva clave: añade una nueva clave a la caché.
    • Buscar una clave: recupera una clave que ya tengas.
    • Borrar caché: elimina todos los pares clave-valor de la caché.
  4. (Solo Memcache dedicado) Consulta la lista de teclas de acceso rápido.

    • Las "claves activas" son claves que reciben más de 100 consultas por segundo (CPS) en la memoria caché.
    • Esta lista incluye hasta 100 teclas de acceso rápido, ordenadas por el valor de CPS más alto.

Gestionar las escrituras simultáneas

Si vas a actualizar el valor de una clave de memcache que puede recibir otras solicitudes de escritura simultáneas, debes usar los métodos de memcache de nivel inferior putIfUntouched y getIdentifiable en lugar de put y get. Los métodos putIfUntouched y getIdentifiable evitan las condiciones de carrera, ya que permiten que varias solicitudes que se gestionan simultáneamente actualicen el valor de la misma clave de memcache de forma atómica.

En el siguiente fragmento de código se muestra una forma de actualizar de forma segura el valor de una clave que puede tener solicitudes de actualización simultáneas de otros clientes:

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

Una mejora que podrías añadir a este código de ejemplo es establecer un límite en el número de reintentos para evitar que se bloquee durante tanto tiempo que se agote el tiempo de espera de tu solicitud de App Engine.

Siguientes pasos