Redisのビットコマンドを用いた高速集計
Redisは様々な型をもっているので
単純な集計(increment)は結構柔軟にできたりします。
ですが、後から条件付きでクロス集計したい時があると思います。
Redisにはビット演算するコマンドがあるので
それを使っていろいろな集計をしてみました。
ビット関連コマンドリファレンス
GETBIT/SETBIT
Redis 2.2.0以降が必要。
ビットのオフセットに対してフラグを取得/設定します。
GETBIT key offset SETBIT key offset value(1/0)
BITOP
Redis 2.6.0以降が必要。
複数のキーに対してビット操作を行うことが出来ます。
集計にはこの操作を使います。
BITOP operation(AND/OR/XOR) destkey key [key ...] BITOP operation(NOT) destkey
簡易Example
127.0.0.1:6379> SETBIT keyA 1 1 (integer) 0 127.0.0.1:6379> SETBIT keyA 2 1 (integer) 0 127.0.0.1:6379> SETBIT keyA 3 1 (integer) 0 127.0.0.1:6379> SETBIT keyB 1 1 (integer) 0 127.0.0.1:6379> BITCOUNT keyA (integer) 3 # keyA のカウント 127.0.0.1:6379> BITCOUNT keyB (integer) 1 # keyB のカウント 127.0.0.1:6379> BITOP AND keyAandB keyA keyB (integer) 1 127.0.0.1:6379> BITCOUNT keyAandB (integer) 1 # keyAandB のカウント 127.0.0.1:6379> BITOP OR keyAorB keyA keyB (integer) 1 127.0.0.1:6379> BITCOUNT keyAorB (integer) 3 # keyAorB のカウント 127.0.0.1:6379> BITOP XOR keyAxorB keyA keyB (integer) 1 127.0.0.1:6379> BITCOUNT keyAxorB (integer) 2 # keyAorB のカウント
ユースケース1:日別集計
ここから具体的なシナリオを考えてみます。
2014/4/1-2014/4/3 に訪れたユーザ数をいくつかの軸で集計してみます。
キーを日別に分けておいて、後でBITOPで演算して集計します。
ビットのoffsetにはユニークユーザID(Integer)を指定します。
登録スクリプト:キーを「2014/4/1〜2014/4/3」に分け、1000ユーザを適当に登録
use strict; use warnings; use Redis; use 5.10.0; my $redis = Redis->new; my @date = qw/ visit:2014-04-01 visit:2014-04-02 visit:2014-04-03 /; sub choice { $_[int(rand(scalar(@_)))] } sub clear { $redis->del($_) for (@date); } sub setbit_all { for my $uid (1..1000) { $redis->multi(); # 適当な確率でvisitする { # 4/1: 9割の確率でvisitさせる if(rand() < 0.9) { $redis->setbit($date[0], $uid, 1, sub {}); } # 4/2: 5割の確率でvisitさせる if(rand() < 0.5) { $redis->setbit($date[1], $uid, 1, sub {}); } # 4/3: 3割の確率でvisitさせる if(rand() < 0.3) { $redis->setbit($date[2], $uid, 1, sub {}); } } $redis->exec(); } } clear(); setbit_all();
集計スクリプト:日付でユーザ集計
A: では日別の結果をORで演算し、3日間全体のユニークユーザ数を集計しています。
B: では4/1と4/3をANDで演算し、2日間どちらも訪れたユニークユーザ数を集計しています。
use strict; use warnings; use Redis; use 5.10.0; my $redis = Redis->new; # 集計する日付 my @date = qw/ visit:2014-04-01 visit:2014-04-02 visit:2014-04-03 /; sub p { my ($key) = @_; printf ("%s %d\n", $key, $redis->bitcount($key)); } sub display { # 全部のbitcount p($_) for (@date); say ''; # bitop # A: 3日間に訪れたユニークユーザ数合計 $redis->bitop('OR', 'visit:total', @date); p('visit:total'); say ''; # B: 2014-04-01 / 2014-04-03 両方に訪れたユーザ数合計 $redis->bitop('AND', 'visit:2day:total', 'visit:2014-04-01', 'visit:2014-04-03'); p('visit:2day:total'); say ''; } display()
ユースケース2:クイズのユーザ集計
クイズを出題し、回答/正解したユーザ数などを集計します。
先程と同じく、ビットのoffsetにはユニークユーザID(Integer)を指定します。
登録スクリプト:キーを「クイズ+選択肢」に分け、1000ユーザを適当に登録
1ユーザに対して性別を登録し、さらに3つの問題をランダムで回答させています。
use strict; use warnings; use Redis; use 5.10.0; my $redis = Redis->new; # 性別 my @genders = qw/man woman/; # 質問に対する回答群 my @option1 = qw/ answer:question1:optionA answer:question1:optionB answer:question1:optionC /; my @option2 = qw/ answer:question2:optionD answer:question2:optionE answer:question2:optionF /; my @option3 = qw/ answer:question3:optionG answer:question3:optionH /; sub choice { $_[int(rand(scalar(@_)))] } sub clear { $redis->del($_) for (@genders, @option1, @option2, @option3); } sub setbit_all { for my $uid (1..1000) { $redis->multi(); { # gender $redis->setbit(choice(@genders), $uid, 1, sub {}); # 3つの問題にランダムで回答 $redis->setbit(choice(@option1), $uid, 1, sub {}); $redis->setbit(choice(@option2), $uid, 1, sub {}); $redis->setbit(choice(@option3), $uid, 1, sub {}); } $redis->exec(); } } clear(); setbit_all();
集計スクリプト:正解したユーザの集計
各問題の正解を定義し、正解したユーザを集計します。
C: では全問正解したユーザを集計しています。
D: では男性の中で全問正解したユーザを集計しています。
use strict; use warnings; use Redis; use Time::HiRes qw/gettimeofday tv_interval/; use 5.10.0; my $redis = Redis->new; # 性別 my @genders = qw/man woman/; # 質問に対する回答群 my @answers = qw/ answer:question1:optionA answer:question1:optionB answer:question1:optionC answer:question2:optionD answer:question2:optionE answer:question2:optionF answer:question3:optionG answer:question3:optionH /; # 以下を正解とする # question1 > optionA # question2 > optionF # question3 > optionH my @collect = qw/ answer:question1:optionA answer:question2:optionF answer:question3:optionH /; sub p { my ($key) = @_; printf ("%s %d\n", $key, $redis->bitcount($key)); } sub display { # 全部のbitcount p($_) for (@genders, @answers); say ''; # bitop # C: 全問正解したユーザ $redis->bitop('AND', 'answer:all:correct', @collect); p('answer:all:correct'); say ''; # D: 男性の中で全問正解したユーザ $redis->bitop('AND', 'answer:man:all:correct', @collect, 'man'); p('answer:man:all:correct'); say ''; } display()
性能評価
BITOPで集計した時の性能評価をしてみました。
環境
サーバ | EC2 |
インスタンスタイプ | c3.large |
Redis | 2.8.9 |
シナリオ:3000万人の集計
先ほどのユースケース2で「3000万人」登録し、BITOPで集計してみました。
結果ログ(Intervalを表示するように改修)
$ perl answer_summary.pl ------------------------------------ man count:14996246 woman count:15003754 answer:question1:optionA count:10003730 answer:question1:optionB count:10001089 answer:question1:optionC count:9995181 answer:question2:optionD count:9999537 answer:question2:optionE count:10002139 answer:question2:optionF count:9998324 answer:question3:optionG count:14998858 answer:question3:optionH count:15001142 ------------------------------------ ------------------------------------ answer:all:correct count:1667467 ---- 0.006544(sec) ------------------------------------ answer:man:all:correct count:833621 ---- 0.006825(sec)
BITOPの結果だけ見ると 6-7ms で終わっています。一瞬ですね。
まとめ
Redisのビットコマンドについて調査してみました。
単純なビット操作なので使い所は限られると思いますが、
Redisだけで高速な集計をしたい場合には有用ではないでしょうか。