TT Lab
はじめる
学ぶ 学習パス コース

Redisとキャッシュ

六つのデータ構造とその居場所

TT Labで続きを見る

一言でいうと

Redisを上手に使うとは、コマンドをたくさん知っていることではなく、問題に合ったデータ構造を選ぶことです。

なぜ必要なのか

多くのチームがRedisをStringだけで使っています。すべてJSONにシリアライズしてSETし、GETします。動作はします。ところが、ユーザープロフィールのメールアドレス1フィールドだけを変えたいときも、全体を読んでパースし、修正して書き直します。同時に2つのリクエストがそうすると、片方が上書きします。

Hashを使えばHSET user:42 email a@b.comの1行で済み、ほかのフィールドと衝突しません。データ構造の選択1つが、競合状態をなくします。

どう動くのか

Stringは単純な値とカウンターです。INCRがアトミックだという点が重要です。閲覧数、レートリミットのカウンター、シーケンス番号に使います。SET key val NX EX 60は、分散ロックの基本形です。

Hashはフィールドが複数あるオブジェクトです。フィールド単位の更新と取得ができ、フィールドが少なければRedisが内部で圧縮されたエンコーディングを使うので、メモリも節約できます。

Listは順序のあるリストで、両端への挿入・削除がO(1)です。ジョブキューと最近のアクティビティ一覧に使います。真ん中へのアクセスはO(N)なので、大きなリストをインデックスで探してはいけません。

Setは重複のない集合で、積集合・和集合・差集合がコマンド1つです。タグ、フォロワー、重複排除に使います。SADDの戻り値で「初めて見るものか」がわかるので、冪等性の実装にも使われます。

ZSet(ソート済み集合)は、スコアを持つ集合です。スコア順に並んだ状態が常に保たれ、順位の取得がO(log N)です。ランキング、優先度キュー、時系列ウィンドウ(スコアをタイムスタンプにして)に使います。このコースでラボを1つ丸ごと割くほど重要です。

Streamは追記専用のログです。コンシューマーグループと確認応答(ack)があり、本物のキューに最も近いです。前のコースで扱いました。

現場での姿

運用でいちばん気をつけるコマンドはKEYSです。キースペース全体を走査し、その間Redisはほかの仕事ができません。Redisは単一スレッドでコマンドを処理するので、キーが数百万個あるインスタンスではKEYS *1回で、数秒の全面停止が起きます。必ずSCANをカーソルで回す必要があります。

同じ理由で、FLUSHALL、大きなコレクションに対するSMEMBERSやLRANGE 0 -1、DELで巨大なキーを消すこと(代わりにUNLINK)にも注意が必要です。「Redisが急に遅くなった」の多くは、O(N)のコマンド1つです。

メモリの観点も知っておく価値があります。MEMORY USAGE <key>でキーごとの実際の使用量が見られ、同じデータでも、データ構造によって何倍も差が出ます。

何を選ぶか決める表

データ構造 使う場面 代表的なコマンド 注意
String キャッシュ値、カウンター SET、INCR 512MBが上限
Hash オブジェクトのフィールドごとの更新 HSET、HGETALL フィールドが多いとHGETALLが重い
List キュー、最近のN件 LPUSH、BRPOP 途中への挿入・取得がO(n)
Set 重複排除、タグ SADD、SINTER 大きな集合の積集合は高くつく
Sorted Set ランキング、時間順のインデックス ZADD、ZRANGEBYSCORE 最も使い道が多い
Stream イベントログ、コンシューマーグループ XADD、XREADGROUP Listよりキューに向く

Sorted Setは意外に幅広く使われます。 スコアをタイムスタンプにすると、時間範囲の取得ができ(ZRANGEBYSCORE)、古いものを消すのも簡単です(ZREMRANGEBYSCORE)。「直近24時間のイベント」のようなものをListで作ろうとすると、すぐ行き詰まります。

キューはListよりStream

Listでキューを作ると、BRPOPで取り出した瞬間にメッセージが消えます。処理中にコンシューマーが死ぬと、そのメッセージはなくなります。

Streamには、コンシューマーグループと確認(ack)の概念があります。

XADD  orders * type payment amount 52000      # 발행
XREADGROUP GROUP workers w1 COUNT 10 STREAMS orders >   # 읽기(pending 으로 표시)
XACK  orders workers <id>                      # 처리 완료
XPENDING orders workers                        # 아직 확인 안 된 것
XCLAIM orders workers w2 60000 <id>            # 죽은 소비자의 것을 가져오기

XPENDINGが核心です。 コンシューマーが死ぬと、そのメッセージはpendingに残り、ほかのコンシューマーがXCLAIMで引き取ります。Listでは、これを自分で作る必要があります。

ただしStreamも際限なく伸びるので、XADD ... MAXLEN ~ 100000のように上限を置きます。~を付けると近似的な切り詰めになり、はるかに安上がりです。

大きなキーが起こす問題

Redisは単一スレッドでコマンドを処理します。1つに時間がかかると、その間すべてが止まります。

KEYS *                      → O(n). 절대 쓰지 않는다
HGETALL (필드 10만 개)      → 응답이 크고 오래 걸린다
SMEMBERS (원소 100만 개)    → 같은 문제
DEL (큰 컬렉션)             → 삭제도 O(n) 이다. UNLINK 를 쓴다

UNLINKは、削除をバックグラウンドに回して、ブロッキングを避けます。大きなキーを消すときは、これを使います。

大きなキーを見つけるのはredis-cli --bigkeysや--memkeysです。定期的に実行して、1つのキーが全体の大きな割合を占めていないかを確認します。

次のラボですること

6つのデータ構造を1つずつ自分で扱い、KEYSの代わりにSCANで走査し、最後に同じデータを別のデータ構造で保存したときのメモリの差を表にします。