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

Redisとキャッシュ

リーダーボードをZSetで実装し検証する

TT Labで続きを見る

目標

ソート済み集合1つで、リアルタイムのリーダーボードの全要件(上位N、個人の順位、ページネーション、同点の規則、期間別ランキング、合算)を実装し、取得性能を数字で確認します。

なぜ重要なのか

ランキングは、リレーショナルDBが最も苦手とすることの1つです。上位10人はインデックスで簡単ですが、「自分の順位」は、自分よりスコアが高い人をすべて数える必要があるので、下位のユーザー1人の取得が99万行を走査します。しかもそのクエリがユーザーごとにリクエストのたびに入り、同時にスコア更新の書き込みが同じインデックスに集中します。ソート済み集合は、スキップリストとハッシュテーブルを一緒に維持して、順位の取得と範囲の取得をどちらもO(log N)にします。100万人でも1000万人でも、ミリ秒です。このラボで特に気をつけるべきところは、ステップ6の同点処理です。Redisのデフォルトの同点規則はメンバー文字列の辞書順ですが、プロダクトの要件はほとんど常に「先に達成した人が上」です。スコアと時刻を1つの実数に畳み込むこの手法は、リーダーボードを作るたびに使うことになります。

ステップ

  1. /root/lb/load.pyで/opt/fixtures/rd/plays.csv(ヘッダーuser_id,points,ts)を読み、lb:globalにユーザーごとの累積スコアを入れます。ZCARD lb:globalが200である必要があります。
  2. 上位10人をスコアと一緒に/root/lb/top10.txtに<순위> <user_id> <점수>形式(プレースホルダーは順位とuser_idとスコアです)で10行書きます。1位が最初の行です。
  3. /root/lb/myrank.txtにユーザーu042の情報をuser=u042 rank=<1부터> score=<점수>の1行で書きます(プレースホルダーは1から数えた順位とスコアです)。
  4. /root/lb/incr.pyでu042に50点をアトミックに加算します。ソースに、ZSCOREで読んでZADDで書くパターンがあってはいけません。実行後、スコアがちょうど50増えている必要があります。
  5. /root/lb/page.txtに100位から109位までの10人を<순위> <user_id>形式(プレースホルダーは順位とuser_idです)で書きます。最初の行の順位が100である必要があります。
  6. /root/lb/tie.pyでlb:tieを作ります。スコアはpoints + (1 - ts / 1e10)で畳み込みます。/opt/fixtures/rd/ties.csvの同点者5人がtsの昇順で上位に来るようにして、結果を/root/lb/tie.txtに5行書きます。
  7. lb:weekly:2026-W34を作り、TTLを604800以下でかけます。ZUNIONSTORE lb:combined 2 lb:global lb:weekly:2026-W34 WEIGHTS 1 2で合算キーを作ります。ZCARD lb:combinedが200以上である必要があります。
  8. /root/lb/api.pyを127.0.0.1:8150で起動します。GET /rank/u042は{"user":"u042","rank":<정수>,"score":<수>,"top3":[...]}を返します(プレースホルダーは整数と数値です)。/root/lb/bench.txtにqueries=1000 total_ms=<정수> avg_ms=<수>を書き(プレースホルダーは整数と数値です)、avg_msは5未満である必要があります。

参考

プレイ記録でリーダーボードを埋める

/root/lb/load.pyで/opt/fixtures/rd/plays.csv(ヘッダーuser_id,points,ts)を読み、lb:globalにユーザーごとの累積スコアを入れてください。ZCARD lb:globalが200である必要があります。

/opt/fixtures/rd/plays.csvを読んで、ユーザーごとのスコアをソート済み集合に入れます。同じユーザーが何度も出てきます。累積する必要があります。

上位10人を取り出す

上位10人をスコアと一緒に/root/lb/top10.txtに<순위> <user_id> <점수>形式(プレースホルダーは順位とuser_idとスコアです)で10行書いてください。1位が最初の行です。

高いスコアから取得する範囲コマンドがあります。スコアも一緒に受け取る必要があり、そうしないとファイルに書けません。

特定のユーザーの順位を求める

/root/lb/myrank.txtにユーザーu042の情報をuser=u042 rank=<1부터> score=<점수>の1行で書いてください(プレースホルダーは1から数えた順位とスコアです)。

順位のコマンドは0から数えます。人に見せるときは、1を足す必要があります。

スコアをアトミックに累積する

/root/lb/incr.pyでu042に50点をアトミックに加算してください。ソースに、ZSCOREで読んでZADDで書くパターンがあってはいけません。実行後、スコアがちょうど50増えている必要があります。

読んで足して書くと、同時リクエストで値が消えます。1つのコマンドで加算する方法があります。

100位台のページを取り出す

/root/lb/page.txtに100位から109位までの10人を<순위> <user_id>形式(プレースホルダーは順位とuser_idです)で書いてください。最初の行の順位が100である必要があります。

範囲コマンドの開始と終了のインデックスを計算してください。100位がインデックスの何番目か、注意してください。

同点を先に達成した順に並べる

/root/lb/tie.pyでlb:tieを作ってください。スコアはpoints + (1 - ts / 1e10)で畳み込みます。/opt/fixtures/rd/ties.csvの同点者5人がtsの昇順で上位に来るようにして、結果を/root/lb/tie.txtに5行書いてください。

スコアと時刻を1つの実数に畳み込みます。小数部が0と1の間でなければ、整数の境界を越えません。

週間ランキングと合算を作る

lb:weekly:2026-W34を作り、TTLを604800以下でかけてください。ZUNIONSTORE lb:combined 2 lb:global lb:weekly:2026-W34 WEIGHTS 1 2で合算キーを作ります。ZCARD lb:combinedが200以上である必要があります。

期間をキー名に入れて期限をかければ、片付けは自動です。2つの集合を重み付きで合算するコマンドがあります。

順位取得APIを立ち上げて性能を測る

/root/lb/api.pyを127.0.0.1:8150で起動してください。GET /rank/u042は{"user":"u042","rank":<정수>,"score":<수>,"top3":[...]}を返します(プレースホルダーは整数と数値です)。/root/lb/bench.txtにqueries=1000 total_ms=<정수> avg_ms=<수>を書き(プレースホルダーは整数と数値です)、avg_msは5未満である必要があります。

これまでに作ったものを1つのエンドポイントにまとめます。1,000回の取得時間を測ると、なぜDBの代わりにこれを使うのかが数字で見えます。