使用 Memcache

本页面介绍如何使用 Google Cloud Console 为您的应用配置和监控 Memcache 服务,此外,还介绍了如何使用 JCache 接口执行常见任务,以及如何使用 Java 版低级别 App Engine Memcache API 来处理并发写入。要详细了解 Memcache,请参阅 Memcache 概览

配置 memcache

  1. 转到 Google Cloud Console 中的 Memcache 页面。
    转到 Memcache 页面
  2. 选择要使用的 Memcache 服务等级:

    • 共享(默认值)- 免费,尽力提供缓存容量。
    • 专用 - 按照缓存大小以 GB 小时计费,提供专门为您的应用分配的固定缓存容量。

    如需详细了解可用的服务类别,请参阅 Memcache 概览

使用 JCache

App Engine Java SDK 支持使用 JCache 接口 (JSR 107) 访问 Memcache。该接口包含在 javax.cache 软件包中。

使用 JCache,您可以设置和获取值、控制值在缓存中失效的方式、检查缓存内容,以及获取缓存的相关统计信息。您还可以在设置和删除值时使用“侦听器”来添加自定义行为。

App Engine 实现尝试实施一个忠实的 JCache API 标准子集。(如需详细了解 JCache,请参阅 JSR 107。)但是,您可能希望考虑使用低级别 Memcache API 来访问底层服务的更多功能,而不使用 JCache。

获取缓存实例

您可以使用 javax.cache.Cache 接口的实现与缓存进行交互。缓存实例可以使用 CacheFactory 获取,而 CacheFactory 可以使用 CacheManager 的静态方法获取。以下代码会获取一个具有默认配置的缓存实例:

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

CacheFactory 的 createCache() 方法采用配置属性的映射。下文对这些属性进行了介绍。要接受默认值,请为该方法提供一个空映射。

添加和获取值

缓存的行为类似于映射:您使用 put() 方法存储键和值,并使用 get() 方法检索值。您可以使用任何可序列化的对象作为键或值。

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

如需添加多个值,您可以调用 putAll() 方法,并使用映射作为其参数。

如需从缓存中移除值(以将其立即逐出),请调用 remove() 方法,并使用键作为其参数。如需从应用的缓存中移除所有值,请调用 clear() 方法。

containsKey() 方法采用键作为参数,并返回 booleantruefalse)来指示缓存中是否存在具有该键的值。isEmpty() 方法可测试缓存是否为空。size() 方法可返回缓存中当前存在的值数。

配置到期时间

默认情况下,所有值都会尽可能长时间地保留在缓存中,除非由于内存不足而被逐出、直接被应用移除或由于其他原因(例如中断)而无法使用。应用可以指定值的过期时间,即值处于可用状态的最长时间。过期时间可以设置为一个相对于值设置时间的时间量,也可以设置为绝对日期和时间。

创建缓存实例时,您可以使用配置属性指定过期政策。随该实例添加的所有值都会使用同一过期政策。例如,要将缓存实例配置为使值在设置一小时(3600 秒)后过期,请运行以下命令:

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

以下属性用于控制值的到期时间:

  • GCacheFactory.EXPIRATION_DELTA:值在给定的时限(相对于其设置时间)后过期(以整数表示,单位为秒)
  • GCacheFactory.EXPIRATION_DELTA_MILLIS:值在给定的时限(相对于其设置时间)后过期(以整数表示,单位为毫秒)
  • GCacheFactory.EXPIRATION:值在给定日期和时间后过期(以 java.util.Date 表示)

配置设置政策

默认情况下,如果缓存中不存在具有给定键的值,则在缓存中设置该值会将其添加到缓存中;如果缓存中已存在具有给定键的值,则在缓存中设置该值会替换原来的值。您可以将缓存配置为仅添加值(保护现有值)或仅替换值(不添加值)。

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

以下属性用于控制设置政策:

  • MemcacheService.SetPolicy.SET_ALWAYS:如果不存在具有该键的值,则添加该值;如果存在具有该键的值,则替换现有值(这是默认设置)
  • MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT:如果不存在具有该键的值,则添加该值;如果存在具有该键的值,则不执行任何操作
  • MemcacheService.SetPolicy.REPLACE_ONLY_IF_PRESENT:如果不存在具有该键的值,则不执行任何操作;如果存在具有该键的值,则替换现有值

检索缓存统计信息

应用可以检索自身的缓存使用情况统计数据。这些统计信息对监控和调节缓存行为会很有帮助。您可以使用 CacheStatistics 对象访问统计信息,该对象可通过调用缓存的 getCacheStatistics() 方法获取。

可用的统计信息包括缓存命中数(获取存在的键)、缓存未命中数(获取不存在的键)以及缓存中的值数。

import javax.cache.CacheStatistics;

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

App Engine 实现不支持重置 hitmiss 计数,这些计数可以无限期地保留,但是可能会由于 Memcache 服务器的暂时性条件而重置。

在 Google Cloud Console 中监控 Memcache

  1. 转到 Google Cloud Console 中的 Memcache 页面。
    转到 Memcache 页面
  2. 查看以下报告:
    • Memcache 服务等级:显示您的应用使用的服务等级为“共享”还是“专用”。如果您是项目的所有者,则可以在两者之间切换。详细了解服务等级
    • 命中率:显示从缓存处理的数据请求的百分比,以及从缓存处理的数据请求的原始数量。
    • 缓存中的内容
    • 最早的内容的存续时间:最早缓存的内容的存续时间。请注意,每次使用内容时(无论是读取还是写入)都会重置其存续时间。
    • 总缓存大小
  3. 您可以执行以下任何操作:

    • 新建键:向缓存添加新键。
    • 查找键:检索现有键。
    • 清空缓存:移除缓存中的所有键值对。
  4. (仅限专用 Memcache 使用)查看热键列表。

    • “热键”是 Memcache 中收到的每秒查询次数 (QPS) 超过 100 的键。
    • 该列表包含最多 100 个热键,按 QPS 的多少降序排列。

处理并发写入

如果要更新可能会收到其他并发写入请求的 Memcache 键值,您必须使用低层级的 Memcache 方法 putIfUntouchedgetIdentifiable 来代替 putgetputIfUntouchedgetIdentifiable 方法允许并发处理的多个请求以原子方式更新同一 Memcache 键的值,从而避免出现竞态条件。

以下代码段展示了如何安全更新可能包含来自其他客户端的并发更新请求的键值:

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

如需对此示例代码进行优化,您可以设置重试次数上限,以避免因阻止时间过长而导致 App Engine 请求超时。

后续步骤