2020/10/21

[Python]redis-pyを使ってRedisの便利機能を実装

pythonredis

概要

WebアプリでRedisを以下の用途でよく使います。

  1. Connection Poolを使う
  2. JWTトークンで使用済のものをblacklistとして登録する
  3. Userに特定種類のAPIのリクエストを複数同時に実行したくない場合のチェックに利用する
  4. DBの負荷が上がってくる場合は、クエリの内容をキャッシュして使用する
  5. 最悪redisが落ちている場合でもアプリケーションが通常動作するようにする

最近はPythonでバックエンドを実装することが多いのでPythonで対応しました。(Python3.8です)
(もちろんgolangでもphpでも何の言語でも同じです。)

実装内容

redis-pyのインストール

% poetry add redis
# redis = "^3.5.3"がインストールされました
# ↓ pipを使う場合は以下
# pip install redis

Connection Pool

  • 以下の init() をWebアプリ起動時に実行しておきます。
  • 個別のコネクション conn() はフレームワークのmiddlewareなどで取得して、http requestのobjectなどに入れておくと便利です
import redis

redis_pool = None


def init():
    global redis_pool
    # ここら辺は設定値を使うと良さげ
    redis_pool = redis.ConnectionPool(
        host="127.0.0.1",
        port=6379,
        db=0,
    )


def conn():
    return redis.StrictRedis(connection_pool=redis_pool)

JWTトークン使用済の制御

単純なGETおよびSETのみを使用します。

デフォルメすると以下のような感じになります。

  • jwt tokenをredisのkeyに含め、jwt tokenの有効期限を使ってredisに格納します。(例えばログアウト時などに使用します)
  • userがrequest時に、もしすでにblacklistに登録済のjwt tokenを使っていたら、ログインできないように制御できます
conn: RedisConn  # 上記のconnection poolから作成されたredisのコネクション

token = "your-jwt-token-value"
prefix = "jwt_token_blacklist_"
key = f"{prefix}{token}"
ttl = 60 * 60 * 24 * 7  # 7 days (seconds)

# setを呼び出してredisに書き込みます。
conn.set(key, "1", ex=ttl)

# getを呼び出してもしblacklistに登録済なら、403などにします。
flg = conn.get(key)
if flg is not None:
    raise YourAuthException("the token can no longer be used.")

リクエストの同時実行制御

  • atomicなgetsetを使って処理します
  • 実行中の場合は1のフラグをredisに立てて、処理が終ったら削除するようにします
  • 不足の自体を考慮して、リクエストの処理が十分に終わると思われる時間でフラグは自動的に消えるようにしておきます
def action:
    # 個別処理を実装しておきます
    return "OK"

user_id = "your-user-identifier"
prefix = "user_request_block_"
key = f"{prefix}{token}"  # pathなどの情報も含めることで、特定のメソッドのAPIだけ同時実行したくないとかもできます
ttl = 10                  # リクエストの処理が十分に終わると思われる秒数を設定しておきます
try:
    blocked = conn.getset(key, "1")
    if blocked == b"1":
        raise YourRuntimeException("Could not execute multiple request at the same time.")
    conn.expire(key, ttl)
    return action()
finally:
    conn.delete(key)

こんな感じでしょうか。

今回は、「DBの負荷が上がってくる場合は、クエリの内容をキャッシュして使用する」tipsは詳しく紹介しませんが
redisに格納するkeyのprefixをうまく設計することで
updateやinsertなどのクエリを実行時に、関係するキャッシュをredisから一括で消去(ワイルドカード“*“を使って)すると便利です。

以上になります。