技術

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)のインストール

1
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起動

1
sudo service redis start

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

テストソース

redis-test.php

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
<?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();

実行

1
php redis-test.php

実行結果の例

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
echoのテスト
成功
 
setのテスト
成功
 
getのテスト
成功
 
deleteのテスト
成功
 
incr, decrのテスト
子プロセス終了
wait終了
成功
 
setexのテスト
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
まだキーが存在する
キーが消えた
成功
 
後掃除
成功

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$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 を使っています。コメントデータの処理方法の詳細はこちらをご覧ください