使用 Memcache

本頁說明如何使用 Google Cloud 控制台設定及監控應用程式的 Memcache 服務。如何使用 JCache 介面執行常見工作,以及如何使用低層集的 App Engine Memcache API (適用於 Java) 處理並行寫入。如要進一步瞭解 Memcache,請參閱 Memcache 總覽

設定 Memcache

  1. 前往 Google Cloud 控制台的「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 取得 Cache 執行個體,CacheFactory 是從 CacheManager 叫用的靜態方法所取得。下列程式碼會取得含預設設定的 Cache 執行個體:

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() 方法採用設定屬性 Map。我們稍後會深入討論這些屬性。如要接受預設值,必須將方法設成空的 Map。

輸入和取得值

Cache 的行為類似於 Map,您可使用 put() 方法儲存鍵和值,並使用 get() 方法擷取值。您可以對鍵或值使用任何 Serializable 物件。

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

若要放入多個值,您可以使用 Map 呼叫 putAll() 方法做為其參數。

若要從快取中移除值 (立即移除),請使用鍵呼叫 remove() 方法做為其參數。如要從應用程式的快取中移除每個值,請呼叫 clear() 方法。

containsKey() 方法採用一個鍵,並傳回 boolean (truefalse),指出快取中是否存在具有該鍵的值。isEmpty() 方法會測試快取是否為空白。size() 方法會傳回目前快取中值的數量。

設定到期時間

根據預設,所有的值都會盡可能保留在快取中,直到記憶體不足、應用程式明確移除值,或其他因素 (例如服務中斷) 才將值移除。應用程式可指定值的到期時間,也就是值的最大可用時間量。到期時間可以是以該值設定時間算起的一個相對時間量,也可以是一個絕對的日期和時間。

您建立 Cache 執行個體時,必須使用設定屬性來指定到期政策。所有與該執行個體一起輸入的值都會使用相同的到期規則。例如下面的範例就是在 Cache 執行個體設為讓值在設定後的一小時 (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

設定 Set Policy

根據預設,在快取中設定值時,如果快取中沒有任何值具有指定的鍵,就會新增該值;如果有,則會取代該值。您可以將快取設定為只新增值 (保護現有的值) 或者只取代值 (不新增)。

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 計數,這些計數會無限期保留,但可能會因記憶快取伺服器暫時出現問題而重設。

在 Google Cloud 主控台中監控 Memcache

  1. 前往 Google Cloud 控制台的「Memcache」頁面。
    前往「Memcache」頁面
  2. 查看下列報表:
    • Memcache 服務等級:顯示應用程式使用的是共用或專屬服務等級。如果您是專案的擁有者,則可以在兩個服務等級之間切換。進一步瞭解服務等級
    • 命中率:顯示從快取提供的資料要求百分比,以及從快取提供的資料要求原始數字。
    • 快取的項目
    • 「Oldest item age」(最舊項目時間長度):最舊快取項目已存在的時間。請注意,項目的存在時間長度會在每次使用 (無論是讀取或寫入) 項目時重設。
    • 「Total cache size」(總快取大小)
  3. 您可以採取下列任何動作:

    • 新增鍵:將新的鍵新增至快取。
    • 尋找鍵:擷取現有鍵。
    • 清除快取:從快取中移除所有鍵/值組合。
  4. (僅限專屬 Memcache) 查詢「熱門鍵」清單。

    • 「熱門鍵」是指 Memcache 中每秒查詢次數 (QPS) 超過 100 的鍵。
    • 此清單最多可包含 100 個熱門鍵,依照 QPS 由高至低排序。

處理並行寫入

如果您要更新可能會接收其他並行寫入要求的 Memcache 鍵值,則必須使用低階 Memcache 方法 putIfUntouchedgetIdentifiable,而非 putget。方法 putIfUntouchedgetIdentifiable 允許並行處理多個要求,以不可分割的形式更新相同 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 要求逾時。

後續步驟