技術

phpからRedisを使ってみよう

何年か前にNoSQL界の新星Redisを使ってみたときは(そんときはPythonから使ってみた)セットアップにちょっと手間取ったような気がするのですが、最近だとずいぶん楽になりましたね。
その間にRedisもNoSQL界の新星からNoSQL界の雄にランクアップした感があります。

ということでPythonからだとどんな感じで使えるのかという事はもう知ってるので、今回はとりあえずphpからRedisを使ってみました。

環境

OS: Amazon Linux 2012.09
php: Package php-5.3.18-1.27.amzn1.x86_64

まずRedisとphp-redis(phpredis)のインストール

sudo yum --enablerepo=epel install redis php-redis

入ったパッケージ
Package redis-2.4.10-1.el6.x86_64
Package php-redis-2.2.2-5.git6f7087f.el6.x86_64

Redis起動

sudo service redis start

デフォルトだと127.0.0.1:6379でredisサーバは待ち受ける。
(設定ファイルは/etc/redis.confにある。)

テストソース

redis-test.php

<?php
$get_redis_connect = function() {
    $redis = new Redis();
    if ($redis->connect('localhost', 6379)) {
        return $redis;
    }
    exit("Error: redisに接続できない\n");
};

$test = function($title, $testfunc) {
    print "\n${title}\n";
    if ($testfunc()) {
        print "成功\n";
    } else {
        print "失敗\n";
    }
    sleep(1);
};

$redis = $get_redis_connect();

$test("echoのテスト", function() use(&$redis) {
    return $redis->echo('えこー') == 'えこー';
});

$test("setのテスト", function() use(&$redis) {
    return $redis->set('hoge', 'fuga');
});

$test("getのテスト", function() use(&$redis) {
    return $redis->get('hoge') == 'fuga';
});

$test("deleteのテスト", function() use(&$redis) {
    $redis->delete('hoge');
    return $redis->exists('hoge') === false;
});

$redis->close();

$test("incr, decrのテスト", function() use(&$get_redis_connect) {
    // マルチプロセスで同時にincr, decrしして整合性をチェックする
    // (非同期に同じ回数incr, decrして元の値に戻るかどうかチェックする)
    $pid = pcntl_fork();
    if ($pid == -1) {
        exit("Error: forkできない\n");
    }
    if ($pid) {
        // 親プロセスではインクリメント
        // Redisオブジェクトはプロセス間で共有出来ないのでここで作成する
        $redis = $get_redis_connect();
        for ($i = 0; $i < 10000; $i++) {
            $redis->incr('hoge');
        }
        $redis->close();
        while (pcntl_waitpid($pid, $status) != -1) {
            sleep(1);
        }
        print "wait終了\n";
    } else {
        // 子プロセスではデクリメント
        // Redisオブジェクトはプロセス間で共有出来ないのでここで作成する
        $redis = $get_redis_connect();
        for ($i = 0; $i < 10000; $i++) {
            $redis->decr('hoge');
        }
        $redis->close();
        exit("子プロセス終了\n");
    }
    
    $redis = $get_redis_connect();
    // redisでは値は全て内部で文字列として格納されてるので
    // 型も含めてチェックするとき(===を使うとき)は気をつける。
    $ret = $redis->get('hoge') === '0';
    $redis->close();
    return $ret;
});

$redis = $get_redis_connect();

$test("setexのテスト", function() use(&$redis) {
    // キーに有効期限をセットして観察してみる
    $ex = 10; // 有効期限(秒)
    $time_start = microtime(true);
    $redis->setex('hoge', $ex, 'fuga');
    while ($redis->exists('hoge')) {
        print "まだキーが存在する\n";
        sleep(1);
    }
    $time_end = microtime(true);
    print "キーが消えた\n";
    return abs($time_end - $time_start - $ex) < 1.5;
});

$test("後掃除", function() use(&$redis) {
    $redis->delete('hoge');
    return true;
});

$redis->close();

実行

php redis-test.php

実行結果の例


echoのテスト
成功

setのテスト
成功

getのテスト
成功

deleteのテスト
成功

incr, decrのテスト
子プロセス終了
wait終了
成功

setexのテスト
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
キーが消えた
成功

後掃除
成功

実はマルチプロセス絡みでちょっとハマった

php-redisのredisオブジェクトはforkするとおかしな事になるみたい。
例えば以下のようなコードの場合
(fork前に生成したredisオブジェクトを、fork後の親プロセスと子プロセスでそのまま使った場合)

$redis = new Redis();
if (!$redis->connect('localhost', 6379)) {
    exit("Error: redisに接続できない\n");
}
$pid = pcntl_fork();
if ($pid == -1) {
    exit("Error: forkできない\n");
}
if ($pid) {
    // 親プロセスではインクリメント
    for ($i = 0; $i < 10000; $i++) {
        $redis->incr('hoge');
    }
    while (pcntl_waitpid($pid, $status) != -1) {
        sleep(1);
    }
    print "wait終了\n";
} else {
    // 子プロセスではデクリメント
    for ($i = 0; $i < 10000; $i++) {
        $redis->decr('hoge');
    }
    exit("子プロセス終了\n");
}

子プロセスではRedisサーバーに未接続という意味でRedisExceptionがスローされる。
が、これはマルチプロセス関係なく未接続の状態でコマンドを発行しようとした場合と同じエラーメッセージが表示されるので分かりやすい。

分かりづらかったのは親プロセス側ではコマンドが適用されたりされなかったりする点。

例では1万回incrコマンドを発行してるが、あとで結果を見ると7千いくつとかまちまちで中途半端な数までしかカウントされてない。
コマンドが部分的に失敗してるような感じだが、失敗したときにRedisExceptionが発生するわけでもなく、incrコマンドの戻り値も全部確認したわけではないが、if文にぶっこんで偽になるような値も返ってきてない様子。

最初「Redisのincr, decrってアトミックだと思ってたけど違うの!?そんなバカな」なんて思ったりしたけど、当然そんな事はなくてphp-redisの使い方が悪かっただけみたい。

ちょっと試行錯誤した結果、fork前に生成したredisオブジェクトを使わず、親プロセス・子プロセス共にfork後に新たにredisオブジェクトを生成することで回避できた。

「php-redisをforkして使う場合は、各プロセスで初期化しよう。」というのを本日の教訓としたいと思います。
(こんなテスト的なコード以外でforkと組み合わせることは一生なさそう)

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください