Redisを使って出入り自由・リアルタイム・日次平均・グループ対抗ランキングを作ろう

みなさん、楽しくランキングを作ってますか?

こんにちは、FiNCで法人向けサービスを作っているエンジニアのyoshikenです。

今回、グループ対抗の歩数ランキングのシステムを作りました。

リアルタイム更新かつ
出入り自由かつ
日次平均かつ
グループ対抗
上記のランキングシステムという少し複雑な要件で実装したので、その際のパターンを各要件のステップごとに分けて説明します!

写真右の枠で囲った部分が実際のランキング部分

目次

  • Lv.0 日次平均歩数ランキングを作ろう!
  • Lv.1リアルタイム歩数ランキングを作ろう!
  • Lv.2リアルタイム日次平均グループ対抗歩数ランキングを作ろう!
  • Lv.3 出入り自由リアルタイム日次平均グループ対抗歩数ランキングを作ろう!
  • まとめ

Lv.0 日次平均歩数ランキングを作ろう!

要件
毎日定時にユーザーごとの歩数が集計されること

実装方法
RDBを使います

テーブルはこんな感じ

例えば歩数が以下のような場合

SQLはこうなりますね

単純ですね!

Lv.1 リアルタイム歩数ランキングを作ろう!

追加要件
リアルタイムで歩数情報を集計してランキングを表示する

実装方法
Redisのsorted_setを使う

Redisのsorted_setとは

ソート済みセット型
Redisソート済みセット型はRedisセット型とよく似ていて、Redis文字列型の集合となっています。違いはソート済みセットのすべてのメンバはスコアに関連したハッシュ値を持っています。元となっているスコアを用いてメンバを順番に並べます。

ハッシュに対するスコアに基づいて更新のタイミングでソートしてくれるので、ランキングが高速に作成できます。

なぜRedisを使うの?

今回扱う歩数データの性質として以下の2つがあります。

  • 書き込み量が読み込みに比べとても多い (FiNCアプリでの歩数データの書き込みはだいたい1hで10万回ほどあります)
  • 一個一個の歩数データは厳密な整合性を求められない

RDBと比較してRedisには以下のような特徴があります。

  • 基本的にインメモリーで動作する
  • そのためプロセスに不具合があった際にはデータの一貫性(Consistency)を担保できないことがある
  • メモリに書き込む分、書き込みのパフォーマンスは早くなる

今回のケースでは書き込みが多く、データの整合性、一貫性が厳密には求められないので、Redisを用います。

実際の処理

例えば飛んでくるイベントの歩数データが以下のような時

Aさんが30歩歩いた
Bさんが20歩歩いた
Cさんが10歩歩いた
Aさんが30歩歩いた
Bさんが20歩歩いた
Cさんが10歩歩いた

Redisでの処理はこのようになります。

*zincrbyはRedisのsorted setのあるメンバのスコアをインクリメントするメソッドです
* zrankはRedisのsorted setのメンバをスコアごとに並べるメソッドです

どれくらいパフォーマンスが違うのか

どれくらいパフォーマンスが違うのか、scriptを作って比較してみました

mysql/Redisのそれぞれに10,000回の歩数情報の書き込みをします。

結果は以下になりました

mysqlでかかったのが1.7s

Redisでかかったので0.7s

Redisの方がパフォーマンスは2倍以上になりました。

Lv.2 リアルタイム更新、日次平均グループランキングを作ろう!

追加要件
今回はリアルタイムという要件に加えて、デイリーで平均を取ったグループ歩数ランキングを作ります。

実装方法
グループの関係性のテーブルは以下です。

Redisで集計するものは

  • グループごとに累計歩数のRedisのhashを作る
  • 累計歩数を経過日数×人数で割ってsorted_setに入れる

例えば

3/23に開始
1グループの参加者は3人

グループAの累計歩数が10,000歩

3/24に飛んでくる歩数データがこんな感じの時

グループAの人が1,000歩歩いた
グループAの人が500歩歩いた
グループAの人が500歩歩いた

処理はこんな感じ

*hincrbyはRedis のhashのメンバをインクリメントするメソッド

これをA~Cのグループで行えばランキングが作成できます。

これでデイリーのグループランキングも実装できました!

Lv.3 出入り自由、リアルタイム更新、日次平均グループランキングを作ろう!

追加要件
グループのメンバーが出たり入ったりします。

実装方法

まず考えること

例えば3/23 ~ 3/25までのグループAの歩数状況がこんな場合

今までのような集計方法で集計すると

  • 方法1) その時点に参加している人数で、歩数全体を割る
    => やめた瞬間に数字が増えてしまう
  • 方法2) 今までに参加した累計人数で、歩数全体を割る場合
    => やめる人が増えると数字が低くなってしまう

という問題があります。

このような問題の対策として、今まで参加している「人×日数」を計算してそれで累計歩数を割る必要があります。

例えば3/25の終了時点でのグループAでの累計参加者数は

3/23のAさんとBさん = 2
3/24のAさんとBさんとCさん = 3
3/25のAさんとCさん = 2

これらの累計なので
2 + 3 + 2 = 7

となります

グループ累計歩数を、グループ累計参加者数で割ります。

3/25終了時点のグループAの平均歩数は
累計の歩数 / 累計の参加者数 = 7,000 / 7 = 1,000

これをRedisのsetを用いて集計することを考えます。

Redisのsetとは

セット型
Redisセット型はRedis文字列型の順不同の集合です。
Redisセットはメンバの重複を許可しないという価値のある性質を持っています。同じ要素を何度も追加しても結果としてセット内にはその要素は単一のコピーしか持ち合わせません。

累計の参加者数は 参加者日時の二つでユニークなkeyをカウントできればOKなので
“Aさん@20190325”のような文字列をRedisのsetに入れます。

Redisのsetはメンバが重複していないことが担保されるので、そのsetの中の総数を取ればそのグループの累計の参加者数を取ることができます。

実際の処理は以下です (上の表の3/25の例)

* sadd はRedisのsetにメンバを追加するメソッド
* scard はRedisのsetのメンバの数(ユニークになっている)を取得するメソッド

あとは累計歩数を累計参加者数で割れば、グループのその日までの平均歩数が計算できます。

これを各グループで行えばランキングができます。

できた! 🎉

まとめ

Lv.0ではRDBのみ
Lv.1ではRedisのsorted_setを
Lv.2ではRedisのsorted_setとhashを
Lv.3ではRedisのsorted_setとhashとsetを
用いて、ランキングを作成しました。

皆さんもぜひRedisの特性を活かして、いろんなランキングを作ってみましょう!