Memcache の使用方法

このページでは、Google Cloud コンソールを使用してアプリケーションの Memcache サービスを構成、モニタリングする方法について説明します。また、JCache インターフェースを使用して一般的なタスクを行う方法と、Memcache の低レベル App Engine API for Java を使用して同時書き込みを処理する方法についても説明します。Memcache については、Memcache の概要をご覧ください。

Memcache を構成する

  1. Google Cloud Console の [Memcache] ページに移動します。
    [Memcache] ページに移動
  2. 使用する Memcache サービスレベルを選択します。

    • 共有(デフォルト) - 無料で、ベスト エフォート ベースでキャッシュ容量を提供します。
    • 専用 - キャッシュ サイズ使用量の GB 時間で請求され、アプリケーションに対して固定キャッシュ容量が独占的に割り当てられます。

    使用可能なサービスクラスについては、Memcache の概要をご覧ください。

JCache の使用

App Engine Java SDK では、Memcache にアクセスするための JCache インターフェース(JSR 107)がサポートされます。インターフェースは javax.cache パッケージに含まれています。

JCache を使用すると、値を設定して取得し、値がキャッシュから期限切れになる方法を制御できます。キャッシュの内容の検査や、キャッシュの統計情報の取得を行うこともできます。また、「リスナー」を使用して、値の設定時や削除時のカスタム動作を追加することもできます。

App Engine では、JCache API 規格のサブセットに準拠した実装を心掛けています(JCache の詳細については、JSR 107 をご覧ください)。ただし、JCache を使用する代わりに、Memcache の低レベル API を使用して、基になるサービスの多くの機能にアクセスすることを検討することもできます。

キャッシュ インスタンスの取得

キャッシュを操作するには、javax.cache.Cache インターフェースの実装を使用します。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() メソッドはキーを受け取って、そのキーの値がキャッシュ内に存在するかどうかを示す booleantrue または false)を返します。isEmpty() メソッドはキャッシュが空かどうかをテストします。size() メソッドは、現在キャッシュ内にある値の数を返します。

有効期限の構成

デフォルトでは、メモリ不足のために強制排除されるか、アプリケーションによって明示的に削除されるか、または別の理由(停止など)のために使用不可になるまで、すべての値が可能な限りキャッシュ内に保持されます。アプリケーションで、値の有効期限つまり値を使用できる最大時間を指定できます。有効期限は値の設定時点からの相対時間数、または絶対日時として設定できます。

キャッシュ インスタンスの作成時に、構成プロパティを使用して有効期限ポリシーを指定します。そのインスタンスで指定されたすべての値で同じ有効期限ポリシーを使用します。たとえば、値が設定された 1 時間(3,600 秒)後に期限切れになるようにキャッシュ インスタンスを構成するには、次のようにします。

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: キーの値が存在しない場合は何も行わず、存在する場合は既存の値を置き換えます。

キャッシュ統計の取得

アプリケーションで、それ自体のキャッシュの使用状況についての統計を取得できます。このような統計情報は、キャッシュの動作をモニタリングして調整するために役立ちます。統計情報にアクセスするには、キャッシュの getCacheStatistics() メソッドを呼び出すことによって取得する CacheStatistics オブジェクトを使用します。

取得可能な統計情報には、キャッシュ ヒットの数(存在していたキーについて取得)、キャッシュミスの数(存在しないキーについて取得)、キャッシュ内の値の数が含まれます。

import javax.cache.CacheStatistics;

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

App Engine の実装では、hit カウントと miss カウントのリセットはサポートされていません。これらのカウントは無期限に維持されますが、Memcache サーバーの一時的な状態のためにリセットされる場合があります。

Google Cloud コンソールで Memcache をモニタリングする

  1. Google Cloud Console の [Memcache] ページに移動します。
    [Memcache] ページに移動
  2. 次のレポートを調べます。
    • Memcache サービスレベル: アプリケーションが共有または専用のどちらのサービスレベルを使用しているかを示します。プロジェクトのオーナーの場合は、2 つのサービスレベルを切り替えることができます。詳細については、サービスレベルをご覧ください。
    • ヒット率: キャッシュから提供されたデータ リクエストのパーセンテージと、そのデータ リクエストの実数が表示されます。
    • キャッシュ内のアイテム
    • 最も古いアイテムの経過期間: 最も古いキャッシュされたアイテムの経過期間。アイテムの経過時間は使用(読み取りまたは書き込み)されるたびにリセットされます。
    • 総キャッシュ サイズ
  3. 次の操作を行うことができます。

    • 新しいキー: 新しいキーをキャッシュに追加します。
    • キーを検索: 既存のキーを取得します。
    • キャッシュをフラッシュ: キャッシュからすべてのキーと値のペアを削除します。
  4. (専用 Memcache のみ)ホットキーのリストを参照します。

    • 「ホットキー」は Memcache 内の 100 クエリ/秒(QPS)以上を受信するキーです。
    • このリストには QPS の高い順で並べ替えられた最大 100 個のホットキーが含まれます。

同時書き込みの処理

他の同時書き込みリクエストを受信することがある Memcache キーの値を更新する場合は、putget ではなく、低レベルの Memcache メソッドである putIfUntouchedgetIdentifiable を使用する必要があります。メソッド putIfUntouchedgetIdentifiable は、同時に処理される複数のリクエストが同じ Memcache キーの値をアトミックに更新できるようにすることで、競合状態を回避します。

次のコード スニペットは、他のクライアントからの同時更新リクエストを受ける可能性のあるキーの値を安全に更新する 1 つの方法を示しています。

@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 リクエストがタイムアウトするような長時間のブロックを防ぐなど、強化策を追加することもできます。

次のステップ