Perl Hackers Hub

第43回PerlでのRedis活用法(2)

前回の(1)こちらから。

Redisのデータ型

次に、Redisの一番の特長であるデータ型について詳しく見ていきましょう。

文字列型

文字列型はRedisで扱うことのできる最もシンプルなデータ型です。バイナリセーフですので、画像を保存する、MessagePackでシリアライズしたデータを保存するといった用途にも使えます。

ただし、日本語の扱いには注意が必要です。日本語を扱うようなPerlスクリプトを書いている場合、スクリプト内にuse utf8プラグマを宣言していると思います。そのような場合、日本語の文字列をそのまま保存しようとすると文字化けしてしまいます。次のコード例のように、文字列のエンコードとデコードを忘れないようにしてください。

use utf8;
use Encode qw(encode_utf8 decode_utf8);

# 書き込むときはエンコード
$redis->set('key' => encode_utf8 '日本語');

# 読み込むときはデコード
my $val = decode_utf8 $redis->get('key');

リスト型

リスト型は複数の値を順番に並べて、インデックス番号でアクセスできるようにした型です。Perlにおける配列と似たようなデータ型ですが、Redisのリスト型は「連結リスト」というデータ構造で実装されていることに注意してください。先頭や末尾への追加や削除といった操作は高速に行えますが、その代わりにインデックス番号でのアクセスは少し苦手です。

# push @key, qw(a b c); に相当
$redis->rpush(key => qw(a b c));

# pop @key; に相当
my $v = $redis->rpop('key');

# $key[1]; に相当
$redis->lindex('key', 1);

リスト型のジョブキューとしての応用

筆者の携わったサービスでは、リストの先頭や末尾への操作が高速である特長を活かして、リスト型を簡易的なジョブキューとして用いていました。

ジョブキューとして利用する場合、ブロックするコマンドがあるというのもRedisの利点です。たとえば、BRPOPコマンドはRPOPコマンドと同様にリストの末尾から値を1つ取り出すコマンドですが、取り出せる値がないときには、リストに値が追加されるのを待ち受けてブロックします。BRPOPコマンドを用いることで、ジョブキューにジョブが投入されるのを待つ処理を簡単に書くことができます。

BRPOPコマンドの簡単なサンプルを用意しました。worker.plはジョブのワーカです。ジョブキューにジョブが投入されるのを待ち、そのジョブを実行します。今回の例ではジョブの内容を画面に出力します。enqueue.plはジョブを投入するプログラムです。

worker.pl
use Redis::Fast;
my $redis = Redis::Fast->new(
    server => 'localhost:6379'
);
while(1) {
    my ($keyname, $job) = $redis->blpop("jobqueue", 30);
    print "$keyname: $job\n";
}
enqueue.pl
use Redis::Fast;
my $redis = Redis::Fast->new(
    server => 'localhost:6379'
);
$redis->rpush("jobqueue", "awesome job");

では、worker.plを実行してみましょう。この時点ではジョブキューに何も入っていないので、画面には何も表示されず、ジョブの待ち状態になります。

$ perl worker.pl

次に、ジョブを投入してみましょう。別の端末を開いて、enqueue.plを実行します。

$ perl enqueue.pl

ジョブが投入されたので、ワーカは待ち状態から抜け、ジョブを実行します。worker.plを実行した端末に受け取ったジョブの内容が表示されるはずです。

jobqueue: awesome job

BRPOPコマンドの動作イメージはつかめたでしょうか。この例ではワーカが1つだけでしたが、worker.plをたくさん起動してからジョブを投入したり、worker.plenqueue.plの実行順を変えたりなどして、実行結果を観察するとより理解が深まるでしょう。

ハッシュ型

ハッシュ型はPerlにおけるハッシュ変数にあたる型で、文字列をキーとする配列です。キーに対応する値には文字列が使用できます。

# %key = (
#     a => 1,
#     b => 2,
# );
# をRedisに書き込む
$redis->hmset( key => (
    a => 1,
    b => 2,
) );

# $key{a} を取得
print $redis->hget('key', 'a');

セット型

セット型は値の集合を扱うための型です。セット型に保存されている値は順序を持たず、同じ要素を重複して保存することはできません。値が集合に含まれているかを高速に判定したい場合に使います。

# a, b, cを含む集合を作成
$redis->sadd(key => qw(a b c));
# 'a'が集合に含まれるか
print $redis->sismember('key', 'a');

同じ要素が重複しないことを利用して、ユニークな要素数(たとえば毎日のユニークユーザー数)を調査するという用途にも使えます。

ただし、セット型を使えば異なり数を正確にカウントできますが、ユニークな要素数が多い場合、大量のメモリが必要になってしまいます。得られるのが近似値でよい場合は、HyperLogLogのほうが有用かもしれません。詳しくはコマンドリファレンスを参照してください。

集合の中からランダムに要素を取得するSRANDMEMBERコマンドも便利です。ゲームではマッチング処理などでランダムな要素取得を多用しますが、通常のRDBでは難しい処理です。そのような処理も簡単に行えるのがRedisの利点です。

次のプログラムはSRANDMEMBERコマンドの利用例です。one、two、threeの3つの要素が入った集合から、ランダムに1つ選びます。

$redis->sadd(myset=>qw(one two three));
print $redis->srandmember('myset');

ソート済みセット型

ソート済みセット型はセット型と同様に集合を扱う型ですが、それぞれの値にスコアを付けることによって、順序付けできるのが特長です。要素の順位を取得したり、あるスコアの範囲にいくつ値が含まれるかを調べたり、といった操作を高速に行えます。

# one: スコア50
# two: スコア100
$redis->zadd( key => (
    50 => one,
    100 => two,
) );

# 'one'の順位を取得
print $redis->zrank('key', 'one');

Luaスクリプト

Redisにはデータ保存機能だけでなく、スクリプト言語Luaの実行環境が組み込まれています。この機能により任意のプログラムをRedis上で実行できます。

利点

Perlを使えば複雑なデータ処理も簡単に書くことができるのに、なぜRedisにプログラムを実行する機能が付いているのでしょうか。

それは、アトミックな処理を書きやすいという理由からです。たとえばWebアプリケーションでは、複数のリクエストを同時に処理することが一般的です。そのため、注意してプログラミングしないと、更新処理が複数同時に走ってしまいデータを壊してしまう可能性があります。Redisでは同時に実行されるスクリプトは1つだけに制限されているため、そういったことを気にしないで済みます。

Luaスクリプトの実行

Luaスクリプトの実行にはEVALコマンドを利用します。たとえばSETコマンドを実行するLuaスクリプトを実行してみましょう。

my $lua = 'return redis.call("set",KEYS[1],ARGV[1])';
$redis->eval($lua, 1, 'key', 'value');

EVALコマンドの引数の「1」は、引数にキーがいくつ含まれるかを表しています。この例の場合は「key」の1つだけなので「1」を指定しています。

Luaスクリプトのキャッシュ

EVALコマンドを使った方法では毎回スクリプトをRedisに転送するため、大きなスクリプトを転送するとネットワーク帯域を多めに使ってしまいます。Redisにはスクリプトをキャッシュしておく機能があるので、それを利用してみましょう。SCRIPT LOADコマンドでスクリプトを保存したあと、EVALSHAコマンドで呼び出せます。

# スクリプトをキャッシュに保存
my $lua = 'return redis.call("set",KEYS[1],ARGV[1])';
my $sha = $redis->script_load($lua);

# 保存したスクリプトを呼び出す
$redis->evalsha($sha, 1, 'key', 'value');

しかし今度は、スクリプトがすでにキャッシュされていた場合にスクリプトが再転送されるので、効率が悪くなってしまいます。EVALコマンドのドキュメントではこの問題を解決するために、EVALSHAを実行してみてNOSCRIPTのエラーが出たら、EVALを実行する」という方法が紹介されています。EVALコマンドはスクリプトの実行と同時にキャッシュも行うので、初回エラーになっても次回からはEVALSHAが成功するようになります。

この処理を簡単に行えるよう筆者が開発したのが、Redis::Scriptモジュールです。Redis::Scriptを利用すると必要なときにだけスクリプトの転送が行われるため、効率的にスクリプトを利用できます。

use Redis::Script;
my $lua = 'return redis.call("set",KEYS[1],ARGV[1])';
my $s = Redis::Script->new(script => $lua);
$s->eval($redis, ['key'], ['value']);

<続きの(3)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧