Memcache の上限を超えるサイズのデータをキャッシュさせたい
Memcache は GAE(Google App Engine)にて標準で利用できる分散型のインメモリ・データ・キャッシュのこと。ちなみに Memcache 自体は GAE に特化したものではないが、ここでは GAE の Memcache サービス について説明する。
GAE の Memcache は非常に便利で強力だが、キャッシュできるデータ値の最大サイズが 1MB という制限がある。が、バッチ処理で呼び出す場合は合計サイズ 32MB まで大丈夫なので、今回は「データを小分けにしてキャッシュに保存し、読み出し時に元に戻す」というアプローチを考えたわけだ。とはいえ、オブジェクトをそのまま分割するわけにはいかないので、一旦シリアライズしてから……えっと、シリアライズというのは……このへんはちょっと説明が難しいので、pickle モジュールのドキュメント から引用する。
"Pickle 化 (Pickling)" は Python のオブジェクト階層をバイトストリームに変換する過程を指します。"非 Pickle 化 (unpickling)" はその逆の操作で、バイトストリームをオブジェクト階層に戻すように変換します。Pickle 化 (及び非 Pickle 化) は、別名 "直列化 (serialization)" や "整列化 (marshalling)"、 "平坦化 (flattening)" として知られていますが、ここでは混乱を避けるため、用語として "Pickle 化" および "非Pickle 化" を使います。
さて、ツイート中でも「結論から先に言」っているが、ここでも結論から先に言うと、以下のようなコードで実現した。あ、言語は Python 2.7 です。
import pickle
from google.appengine.api import memcache
from app.models import Contact
CHUNK_SIZE = 500000
def fetch_contacts():
contacts = Contact.query().fetch()
serialized = pickle.dumps(contacts, 2)
values = {}
for i in xrange(0, len(serialized), CHUNK_SIZE):
values['Contact_{0}'.format(i // CHUNK_SIZE)] = serialized[i : i + CHUNK_SIZE]
memcache.set_multi(values)
return contacts
cached_list = memcache.get_multi(['Contact_{0}'.format(i) for i in xrange(20)])
if cached_list:
try:
contacts = pickle.loads(''.join([value for key, value in sorted(cached_list.items()) if value is not None]))
except Exception as e:
contacts = fetch_contacts()
else:
contacts = fetch_contacts()
# 以降、contacts の値をよしなに扱う
今回使っているのは pickle 化された文字列を直接扱う pickle.dumps()
と pickle.loads()
の2つ。前者 pickle.dumps()
の第2引数の 2 がちょっと分かりにくいかもしれないが、これはプロトコルと呼ばれるもので、数値(バージョン)が大きくなるほどより効率的な pickle 化が行なえる反面、下のプロトコルとの互換性がなくなる。そしてバージョン 3 は Python 3 で導入されたため、今回の Python 2.7 環境ではエラーとなってしまう。よって、2 を指定しているわけだ。