サンプルアプリケーション Mini Twitter を改修し、Redis 連携を行うようにします。
目次
AWS 基礎入門チュートリアルについて
このページは AWS 基礎入門チュートリアル の一部です。 AWS チュートリアル全体は AWS 基礎入門チュートリアル を参照してください。
PHP から Redis にアクセス
前ページにて Elasticache を導入し、Redis でアクセスできることを確認しました。
今度は PHP から Redis にアクセスできるようにしてみましょう。 PHP から Redis にアクセスする場合、Predis というモジュールと、 phpredis というモジュールがあるようですが、 ここでは phpredis を使うことにします (インストールに成功したのが phpredis だったから、 という以外の理由はありません)。
amazon-linux-extras に epel をインストールします。
% sudo amazon-linux-extras install epel
すると下記のようになるはずです。
次に remi をインストールします。 remi とは、追加リポジトリで、RedHat にて働いているフランスの Remi さんがメンテしている模様。
% sudo rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
次に php72-php-pecl-redis4 パッケージをインストールします。 意味は「PHP7.2 用の、PHP から Reids4 を使うための PECL にあるパッケージ」です。 PECL とは “PHP Extention Community Library” の略で、 PHP の拡張モジュール集のようです。
% sudo yum -y install php72-php-pecl-redis4.x86_64
これだけだとなぜかライブラリが見つからないとエラーになるため、 下記でシンボリックリンクを張りました。 普通はこういうことはしませんので、より正しい手順があるのだと思いますが…。
sudo ln -s /opt/remi/php72/root/usr/lib64/php/modules/redis.so /usr/lib64/php/modules
sudo ln -s /opt/remi/php72/root/usr/lib64/php/modules/igbinary.so /usr/lib64/php/modules
さらに sudo vi /etc/php.ini などとして /etc/php.ini を編集します。
/etc/php.ini に
;;;;
; Note: packaged extension modules are now loaded via the .ini files
; found in the directory /etc/php.d; these are loaded by default.
;;;;
という箇所がありますので、 その下あたりに下記 2行を追加してください (実際は “[PHP]” セクション配下ならどこでもよいです)。
extension=igbinary.so
extension=redis.so
接続テストプログラム
PHP から Redis に接続するテストプログラムを準備します。処理概要は下記のとおりです。
- Redis に接続する
- キーを 5個 set する (それぞれ expire 秒数を変える)
- 2秒 sleep
- セットしたキーを 5個 get する
connect の “myredis.*.cache.amazonaws.com” の部分は、 ご自身の ElastiCache インスタンスの「プライマリエンドポイント」の欄にあわせて修正してください。
下記がソースです。
<?php
$redis = new Redis();
$redis->connect('myredis.gh65zs.0001.use2.cache.amazonaws.com', 6379);
echo $redis->ping() . "\n";
for ( $i=0 ; $i<5 ; $i++ ){
print "set key:key$i value:value$i expire:$i\n";
$redis->set("key$i", "value$i", $i);
}
print "sleeping 2 seconds\n";
sleep(2);
for ( $i=0 ; $i<5 ; $i++ ){
$val = $redis->get("key$i");
if ( $val === false ){
print "get key:key$i is failed\n";
} else {
print "get key:key$i value:$val\n";
}
}
下記のように実行します。
% php ./redis-test.php
+PONG
set key:key0 value:value0 expire:0
set key:key1 value:value1 expire:1
set key:key2 value:value2 expire:2
set key:key3 value:value3 expire:3
set key:key4 value:value4 expire:4
sleeping 2 seconds
get key:key0 is failed
get key:key1 is failed
get key:key2 is failed
get key:key3 value:value3
get key:key4 value:value4
この結果から以下のようなことがわかります
- ping すると PING と返ってくる
- キーにセットした値を取得できている
- expire 秒数をすぎると値が取得できなくなる (Redis 上から消えている)
- 値の取得に失敗したら false が返ってくる
サンプルアプリ Mini Twitter に Redis キャッシュ機能を追加
minitwitter-redis.php こちら をダウンロードしてください。
変更点は主に 3つです。 1つめは、毎回下記のように Redis 接続を行うこと。
29: // Redis 接続
30: $redis = new Redis();
31: $redis->connect('myredis.gh65zs.0001.use2.cache.amazonaws.com', 6379);
2つめは、これまで自身のツィート数やフォロワー数を求める際、下記のように常に SQL 文を発行していました。
79: $stmt = $dbh->prepare('select count(*) as tweet_count, sleep(1) from tweet where account_id = ?');
80: $stmt->execute(array($account_id));
81: $res = $stmt->fetch();
82: $tweet_count = $res['tweet_count'];
これを
- まずは Redis より取得する。
- 狙いのデータが存在しなければ DB より取得し、結果を Redis に設定する (次回アクセス時の高速化を目的として))。
- Redis に設定したキーは 10秒で自動的に削除される (expire)。
という方針に変更し、下記のように修正しました。
89: $tweet_count = $redis->get("tweet_count_$account_id");
90: if ( $tweet_count === false ){
91: $stmt = $dbh->prepare('select count(*) as tweet_count, sleep(1) from tweet where account_id = ?');
92: $stmt->execute(array($account_id));
93: $res = $stmt->fetch();
94: $tweet_count = $res['tweet_count'];
95: $tweet_count_debug_message = "ツイート数をDBより取得";
96: $redis->set("tweet_count_$account_id", $tweet_count, 10);
97: } else {
98: $tweet_count_debug_message = "ツイート数をRedisより取得";
99: }
フォロワー数取得部分も同様に修正しました。
101: $follow_count = $redis->get("follow_count_$account_id");
102: if ( $follow_count === false ){
103: $stmt = $dbh->prepare('select count(*) as follow_count, sleep(1) from follow where account_id_follower = ?');
104: $stmt->execute(array($account_id));
105: $res = $stmt->fetch();
106: $follow_count = $res['follow_count'];
107: $follow_count_debug_message = "フォロー数をDBより取得";
108: $redis->set("follow_count_$account_id", $follow_count, 10);
109: } else {
110: $follow_count_debug_message = "フォロー数をRedisより取得";
111: }
3点目。新規ツィート後、この後に自身のツィート数をカウントするのですが、 Redis に保存されているであろうツィート数に関するキーを削除するようにしました。 これを行わないと Reids にキーが保存されている場合、ツィート数が 1つだけ少なく表示されてしまうためです。
67: // レコード INSERT。
68: $sql = "INSERT INTO tweet (account_id, message) VALUES (:account_id, :message)";
69: $stmt = $dbh->prepare($sql);
70:
71: $stmt->bindParam(':account_id', $account_id, PDO::PARAM_INT);
72: $stmt->bindParam(':message', $new_message, PDO::PARAM_STR);
73: $res = $stmt->execute();
74: $dbh->commit();
75:
76: // Redis からツィート数に関するキーを削除
77: $redis->delete("tweet_count_$account_id");
Ʊ様にフォローしたり、フォローを解除したりした際、 INSERT や DELETE を行っていた箇所について、 最後に Redis からキーを削除 (delete) するようにしました。 これにより、A さんが B さんをフォローしたり、A さんが B さんのフォローをやめたりしたとき、 B さん画面には最新のフォロワー数が表示されるようになります。
43: if ( $follow_mode == 'follow' ){
44: // フォロー処理
45: $sql = "insert ignore into follow (account_id_followee, account_id_follower) values (?, ?)";
46: $stmt = $dbh->prepare($sql);
47: $stmt->execute(array($account_id_followee, $account_id));
48: $dbh->commit();
49:
50: } else if ( $follow_mode == 'unfollow' ){
51: // フォロー解除処理
52: $sql = "delete from follow where account_id_followee = ? and account_id_follower = ?";
53: $stmt = $dbh->prepare($sql);
54: $stmt->execute(array($account_id_followee, $account_id));
55: $dbh->commit();
56: }
57: // 被フォロワーの Redis キーを削除
58: $redis->delete("follow_count_$account_id");
結果
この修正により、毎回 2秒以上かかっていたツィッター画面の表示について以下のように高速化が実現できました。
- Redis 保持期間である 10秒以内のアクセスであれば、負荷が高い DB にアクセスせず、Redis から情報を取得する。
- その結果、Redis 取得時は処理が速い。
一方で以下のような欠点もあります。
- 10秒経過した後は、結局 DB を参照しにいってしまい、遅い。
- 自分をフォローしている人の数 (フォロワー数) は最大 10秒の遅延が発生する。
ElastiCache について触れなかったこと
ElastiCache の Redis について下記は触れませんでした。今後追記していきたいと思います。
- 複数インスタンスでのクラスタリング
- データバックアップ
- 容量管理
- String 以外のデータ型 (Hash や List 等)