Flask-CachingでRedisにキャッシュする

Flaskアプリケーションでビューを楽にキャッシュする方法はないかと探していた時、同僚にFlask-Cachingを紹介されました。
Flask-Cachingを使ってRedisにキャッシュする方法について整理します。

Flask-Caching

Flask-Cachingは以下の特徴があり、Flaskアプリケーションに容易に導入できます (公式ドキュメント) 。

  • デコレータで簡単にキャッシュの設定・定義ができる
  • Redis、memcached、ファイルなど、複数の種類のキャッシュミドルウェアを同じインタフェースで利用できる

github.com

Redisの構築 (準備)

キャッシュ先として利用するRedis環境をDockerで構築しておきます。

>  docker run -d -p 6379:6379 redis --requirepass pass1234

サンプルアプリケーションの実装 (準備)

必要なパッケージをインストールしておきます。

> pip install flask==1.0.2 Flask-Caching==1.4.0 redis==2.10.6

今回題材とするFlaskアプリケーションの実装は以下です。

  • /<key>/<value>にPOSTリクエストすると、dictオブジェクトに保存されます (ここではグローバル変数DICTIONARY、実際のサービスではDBなどが使われるでしょう)
  • /<key>にGETリクエストすると、永続化層からkeyの値を取得して返します

FLASK_APP=run.py ./venv/bin/flask runで起動します。

from flask import Flask

DICTIONARY = {}  # DBでもなんでも良いです

app = Flask(__name__)


@app.route('/<key>')
def get_value(key):
    print(f'get_value({key})')
    return f'key={key}, value={DICTIONARY[key]}'


@app.route('/<key>/<value>', methods=['POST'])
def set_value(key, value):
    print(f'set_value({key},{value})')
    DICTIONARY[key] = value
    return 'OK'

Flask-Cachingの導入

それではFlask-Cachingをアプリケーションへ導入します。目的は/<key>へのGETアクセスのレスポンスをキャッシュすることです。ポイントは2点です。

  • Cacheインスタンスを作成します
    • 上で構築したRedisサーバの設定をconfigに渡し、Flaskインスタンス (app) と紐付けます
    • 設定項目の一覧は、こちら
  • キャッシュしたいハンドラをデコレータ (@cache.cached()) で指定します
from flask import Flask
from flask_caching import Cache

DICTIONARY = {}  # DBでもなんでも良いです

app = Flask(__name__)

# Cacheインスタンスの作成
cache = Cache(app, config={
    'CACHE_TYPE': 'redis',
    'CACHE_DEFAULT_TIMEOUT': 60,
    'CACHE_REDIS_HOST': 'localhost',
    'CACHE_REDIS_PORT': 6379,
    'CACHE_REDIS_PASSWORD': 'pass1234',
    'CACHE_REDIS_DB': '0'
})


@app.route('/<key>')
@cache.cached(timeout=30)  # 30秒間レスポンスをキャッシュする
def get_value(key):
    print(f'get_value({key})')
    return f'key={key}, value={DICTIONARY[key]}'

# 以下略

起動して、/key1/1にPOSTしてデータを作り、/key1にGETすると値1が取得できます。このときキャッシュが行われます。
最初のアクセスではprintで出力されますが、2回目以降のアクセスでは出力されません。30秒 (timeoutの設定値) が経過後に、GETリクエストするとキャッシュは捨てられるのでDICTIONARYから取得してビューが生成されます (print文も出力されます) 。

GET後にRedisに接続して中を見ると、キーflask_cache_view//key1に対して、値!\x80\x03X\x11\x00\x00\x00key=key1, value=1q\x00.が作られていることがわかります。
キーは、デフォルトではflask_cache_view/ + パスとなっています。この内、flask_cache_はCacheインスタンス作成時の設定項目の一つ、CACHE_KEY_PREFIXで変更できます。

127.0.0.1:6379> KEYS *
1) "flask_cache_view//key1"
127.0.0.1:6379> GET flask_cache_view//key1
"!\x80\x03X\x11\x00\x00\x00key=key1, value=1q\x00."

cachedデコレータにはいくつかパラメータがあります。

  • timeout: キャッシュの有効期間 (秒)
  • key_prefix: その名の通りキーのプリフィックスで、デフォルトでは"view/リクエストパス"
  • query_string: Trueの場合、クエリパラメータが異なるリクエストは、クエリパラメータを含むハッシュ値をキーとしてキャッシュされます (デフォルトはFalseで、クエリパラメータが異なっても同一のキーが使われる)

キャッシュの削除

次はキャッシュを削除することを考えます。
上のままでは、GETしてキャッシュをした後30秒間は常にキャッシュの値が返されます。30秒以内にPOSTして値が更新されたとしても、30秒が経過するまで更新後の値を取得できません (古い値が返されます) 。

キャッシュの削除はCacheオブジェクトのdeleteメソッドで行います。POSTでキャッシュを削除するように変更したset_valueメソッドを示します。

Redisのキーは flask_cache_view//+key という形式となっています。flask_cache_ (CACHE_KEY_PREFIX) の部分はdeleteメソッド内で補完されますので、cache.delete(f'view//{key}')とすることでGETで設定されたキーを削除できます。

@app.route('/<key>/<value>', methods=['POST'])
def set_value(key, value):
    print(f'set_value({key},{value})')
    DICTIONARY[key] = value

    cache.delete(f'view//{key}')

    return 'OK'

まとめ

FlaskでレスポンスをRedisにキャッシュする簡単な方法として、Flask-Cachingを紹介しました。