Perl Hackers Hub

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

(1)こちら⁠2)こちらから。

Redisの活用例

Redisの基本的な機能を見たところで、アプリケーションでの活用例を紹介します。CPANにはRedisを使ったモジュールも数多くアップロードされており、それらを利用することでRedisを簡単に活用できます。そのようなモジュールも一緒に見ていきましょう。

セッションストアやキャッシュ

Redisはデータを一時的に保存するのに適しているので、Webアプリケーションのセッションストアやキャッシュとして利用するとよいでしょう。

ただし、Redisが扱えるのは単純な文字列だけですので、複雑な構造を持ったデータを直接保存することはできません。⁠Redisのデータ型」で紹介したようにハッシュ型を扱うことはできますが、保存できる値はやはり文字列だけです。

ネストしたハッシュや配列のような複雑なデータをRedisに保存したい場合は、適切な方法でシリアライズする必要があります。そんなときにはCache::Redisモジュールを使うのが便利です。次のようにハッシュでもRedisに保存できます。

my $redis = Redis::Fast->new;
my $cache = Cache::Redis->new(
    redis => $redis
);
$cache->set('key' => {hoge => 1});
print $cache->get('key')->{hoge};

また、Cache::Redis はPerl で広く使われているCache::Cacheインタフェースと互換性があるという利点もあります。そのためほかのCPANモジュールとの連携も容易です。

リアルタイムランキング

ソート済みセット型は順位の計算を高速に行えるので、リアルタイムランキングを作るのに便利です。

ただし、スコアが同じ要素でも同順位とはならず、辞書順で早い要素が高い順位とみなされることに注意しましょう。本稿執筆時点(2017年1月)では、Redis自体に同順位を扱うコマンドはありません。

ランキングの目的によっては、同スコアの要素は同順位となったほうが望ましい場合も多いでしょう。そのような場合はPerl側で、⁠スコアを取得したあとで、それより高いスコアの要素数を調べる」といった工夫が必要です。Redis::LeaderBoardモジュールを使うと、同順位を考慮したランキングを簡単に扱えます。

my $redis = Redis::Fast->new;
my $lb = Redis::LeaderBoard->new(
    redis => $redis,
    key => 'leader_board:1',
    order => 'asc', # asc/desc
);
$lb->set_score(one => 100);
$lb->set_score(two => 50);
my ($rank, $score) =
    $lb->get_rank_with_score('one');

場合によっては、⁠同スコアのときは早いもの順で順位を決める」のように、スコアが同じだった場合の第2基準を設けたいこともあるでしょう。しかしRedis::LeaderBoardで指定できるスコアは1つだけですので、このようなルールには対応できません。そこで、複数のスコアを指定できるよう筆者が拡張を行ったRedis::LeaderBoardMultiというモジュールもあります。Redis::LeaderBoardと同様のインタフェースで使えるので、利用を検討してみてください。

排他制御

cronを使ってジョブを実行する場合などに、多重実行を制御したいことがあると思います。このような場合はsetlockコマンドがよく使われますが、ホストをまたいでの排他制御は行えません。これをRedisを利用して実現するのが、Redis::Setlockです。

次のようなコマンドで、programを排他的に実行できます。

$ redis-setlock KEY program

ライブラリとして利用することもできます。

my $redis = Redis::Fast->new;
my $g = Redis::Setlock->lock_guard($redis,'key');
if ($g) {
    # ロック獲得成功
}
else {
    # ロック獲得失敗
}

Redisのテスト

実際のサービスでRedisを活用するのであれば、正しく動くかを確認するために自動テストを書くのが望ましいです。テストであってもプロダクション環境に近いほうがよいので、実際にRedisサーバに接続しましょう。

開発用にRedisサーバを準備してもよいのですが、余計なデータがRedisサーバに残っていると、データの整合性が取れなくなってしまいます。それを避けるためにも、テストのたびにクリーンなRedisサーバを起動しましょう。次のようにTest::RedisServerモジュールを利用すると、テスト用のRedisサーバを簡単に起動できます。

use Redis::Fast;
use Test::RedisServer;
use Test::More;

# テスト用のRedisサーバを立ち上げる
my $redis_server = Test::RedisServer->new;

# テスト用のRedisサーバに接続する
my $redis = Redis::Fast->new(
    $redis_server->connect_info
);

# Redisを使ったテスト
is $redis->ping, 'PONG', 'ping pong ok';

done_testing;

Redisのキーを便利に使うユーティリティ

Redisは便利なKVSですが、Redisを利用するすべてのプログラムから自由に読み書きができるため、見方を変えればシステム全体で使える強力なグローバル変数であるとも言えます。安易にキーの名前を決めると、ほかのキーと衝突する可能性もあるので、適切に管理したいところです。本節ではそれを解決するモジュールを紹介します。

Redis::Namespace─⁠─キー名への接頭辞付与を自動化する

Redisではキー名に接頭辞を付けて、ネームスペースを表現することが推奨されています。たとえば、ユーザーに関するデータを保存するキーは「user:」で始める、セッション情報を保存するキーは「session:」で始めるといった具合です。

接頭辞を付ける作業を毎回手動で行っていると、タイプミスをしてしまったり、接頭辞を付け忘れたりしてしまう可能性があります。Redis::Namespaceモジュールはこういったミスを防ぐために、キー名への接頭辞付与を自動的に行います。

Redis::NamespaceはRedis::Fastとインタフェースの互換性があるので、次のコードのように、Redis::Fastを直接使う場合とまったく同じ書き方で利用できます。

my $redis = Redis::Fast->new;
my $ns = Redis::Namespace->new(
    redis => $redis,
    namespace => 'ns',
);

# $redis->set('ns:key', 'value');
$ns->set('key' => 'value');

# $redis->get('ns:key');
print $ns->get('key');

Redis::Namespaceのインタフェース互換性を応用すると、1つのRedisサーバを複数のアプリケーションから共有するという、おもしろい使い方もできます。たとえば、アプリケーションApp1には「app1:」というネームスペースを、アプリケーションApp2には「app2:」というネームスペースを付けるようにすれば、App1とApp2で使われるキー名が被ることはありません。こうすることで、Redisサーバを共有していることをまったく意識せずに、それぞれのアプリケーションを開発できます。もちろん、プロダクション環境ではそれぞれのアプリケーションに個別のRedisサーバを割り当てるべきですが、開発環境など処理速度や信頼性が多少低くても問題ない用途では有用でしょう。

Redis::Key─⁠─Redisのキーのラッパモジュール

「user:ユーザーID」のような形式で、キー名の一部にユーザーIDなどを埋め込みたい場合があります。そのような場合に便利なのがRedis::Keyモジュールです。

Redis::KeyはRedisのキーのラッパモジュールです。プレースホルダを使って「user:{id}」のようにキー名のテンプレートを作ることができます。もちろん、Perl組込みのsprintfを用いたり、正規表現を利用してもよいのですが、Redis::Keyは引数に名前を付けられるのでキー名をわかりやすく管理できます。また、⁠キーの名前」「Redisの接続先」を一括して管理できるため、同じキーへの操作を簡潔に書けるのも利点です。

# キー名のルールを決める
my $redis = Redis->new;
my $user = Redis::Key->new(
    redis => $redis,
    key => 'user:{id}',
    need_bind => 1,
);

# キー名にIDを埋め込む
my $key = $user->bind(id => 123);

# $redis->set('user:123', 'value');
$key->set('value');

# $redis->get('user:123');
$key->get;

Redisを利用するうえでの注意点

Redisは強力ですが、使い方によっては思わぬトラブルを引き起こします。そこで最後に、利用するうえでの注意点を整理します。

計算量

最近のコンピュータはマルチコアのものが主流ですが、Redisはシングルコアしか使えません。そのため、Redisに時間のかかる処理をさせると、ほかのすべての処理が止まってしまいます。Redisのドキュメントには各コマンドについて、それぞれにどの程度の計算量が必要となるのか「O(1)」⁠O(n)」といった形で明記されています。これを参考にしてコマンドが十分に短い時間で終わるよう心がけましょう。

たとえば、リスト型の先頭要素を操作するLPUSHコマンドやLPOPコマンドの計算量はO(1)です。これは、データ量に依存せず一定の時間でコマンド実行できるという意味です。一方、LINDEXコマンドはインデックス番号を指定してリスト型の要素を取得するコマンドですが、このコマンドの計算量はO(n)です。こちらはデータ量に正比例した時間がかかります。

両者の違いを見るためのベンチマークプログラムを書いてみました。

use strict;
use warnings;
use Parallel::Benchmark;
use Test::RedisServer;
use Redis::Fast;
use Log::Minimal;

my $redis_server = Test::RedisServer->new;
my $redis = Redis::Fast->new(
    $redis_server->connect_info
);

my @n = (
            1,
           10,
          100,
        1_000,
       10_000,
      100_000,
    1_000_000,
);

sub bench_lindex {
    my $count = shift;
    my $bm = Parallel::Benchmark->new(
        setup => sub {
            my ($self, $id) = @_;
            $redis->del("key:$id");
            $redis->lpush("key:$id", 1..$count);
        },
        benchmark => sub {
            my ($self, $id) = @_;
            $redis->lindex("key:$id", $count-1);
            return 1; # lindex
        },
        concurrency => 4,
    );

    $bm->run();
}
for my $count(@n) {
    infof 'lindex: %d', $count;
    bench_lindex $count;
}

sub bench_lpushpop {
    my $count = shift;
    my $bm = Parallel::Benchmark->new(
        setup => sub {
            my ($self, $id) = @_;
            $redis->del("key:$id");
            $redis->lpush("key:$id", 1..$count);
        },
        benchmark => sub {
            my ($self, $id) = @_;
            $redis->lpush("key:$id", 'a');
            $redis->lpop("key:$id");
            return 2; # lpush + lpop
        },
        concurrency => 4,
    );

    $bm->run();
}

for my $count(@n) {
    infof 'lpush & lpop: %d', $count;
    bench_lpushpop $count;
}

表1はMacBook Air(13-inch, Mid 2012)での実行結果です。少し古いPCでの計測なので数字はあくまでも参考値ですが、傾向はつかめると思います。O(1)のLPUSHコマンドやLPOPコマンドはデータ量の大小にかかわらず速度はほとんど変わりませんが、O(n)のLINDEXコマンドはデータ量が大きくなると遅くなっていることがわかります。

表1 リスト型のベンチマーク結果
リストの長さLINDEXコマンドの
1秒あたりの実行数
LPUSH、LPOPコマンド
の1秒あたりの実行数
155,78155,884
1052,95155,535
10053,71552,476
1,00051,02556,889
10,00025,95456,636
100,0003,17155,930
1,000,00031553,764

このような計算量の違いを意識しておかないと、開発中はデータが小さく問題にならないけれど、本番運用ではデータが多すぎて時間がかかる、ということになりかねません。特に計算量がO(n)のコマンドを使う場合には、あまりデータ量が大きくならないよう注意してください。

メモリ使用量

Redisはインメモリで動作するため、その性能を活かすには十分なメモリが必要です。メモリが不足するとOOM KillerOut of Memory Killerがほかのプログラムを強制終了してしまいます。それを避けるためにも、事前に必要なメモリ使用量を計算し、十分なメモリを準備しましょう。

さらに、Linux上ではディスクに保存する際にforkが行われるため、一時的に2倍のメモリを確保します。CoWCopy on Writeのおかげで実際には2倍のメモリは必要ありませんが、OOM Killerに強制終了されてしまう場合があります。この動作はOSの設定で変更できます。RedisのFAQに詳しい説明があるので、そちらを参照してください。

まとめ

Redisを活用するための代表的なCPANモジュールと、利用するうえでの注意点を説明しました。RedisにはPubSubやHyperLogLog、GEO APIなど、紙幅の都合で紹介できなかったおもしろい機能がまだたくさんあります。興味のある人は、公式ページにあるチュートリアルを読むとよいでしょう。本稿をきっかけに、Redisを利用する人が増えるとうれしいです。

さて、次回の執筆者は本連載の監修者の一人である大沢和宏さんで、テーマは「LINE Messaging API」です。お楽しみに。

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

おすすめ記事

記事・ニュース一覧