RedisをRubyから触ってみた

前回、多機能高速なキーバリューストアRedisをインストールして、本体ソースについてきたクライアントで値を保存したり取得したりしてみました。

前回の記事

今回は同じ事をRubyからやってみたいと思います。
あと、最後の方でRuby+Redisでツイッターのタイムライン的なものを表現してみたり。

インストール
ライブラリはgemで提供されています。

$ gem install redis

使い方
Redis.newでオブジェクトを生成して、あとはRedisのコマンド(http://redis.shibu.jp/commandreference/index.html)にならってメソッドが実装してあるようです。

ローカルホストの標準ポートでRedisが動いている場合

redis = Redis.new

ホストとポートを指定する場合

redis = Redis.new(:host => "10.0.1.1", :port => 6380)

Unixドメインソケットを使用する場合

redis = Redis.new(:path => "/tmp/redis.sock")


値の保存と取得

Rubyライブラリ側で値をセットして取得してみる

redis.set "foo", "bar" 
=> "OK"
redis.get "foo"
=> "bar"

redis本体に付属している標準クライアント側で値を取得

> get "foo"
"bar"

標準クライアント側で値を更新する

> set "foo" "baz"
OK

Ruby側で取得

redis.get "foo"
=> "baz"

もう一つキーと値のペアを追加

redis.set "hoge", "tara"
=> "OK"

複数キーを指定していっぺんに値の配列を取得

redis.mget "hoge", "foo"
=> ["tara", "baz"]

リスト操作

リストの末尾に値を追加

redis.rpush "tweets", "this is my first tweet."
=> 1

リストの0番目から100番目までの値を取得(実際には存在する数だけ取得する)

redis.lrange "tweets", 0, 100
=> ["this is my first tweet."]

もうひとつ値を末尾に追加

redis.rpush "tweets", "hello world!"
=> 2

0番目から100番目までの値を取得(実際には存在する数だけ取得する)

redis.lrange "tweets", 0, 100
=> ["this is my first tweet.", "hello world!"]

redis本体に付属している標準クライアントからリストを取得してみる

> lrange "tweets" 0  100
1) "this is my first tweet."
2) "hello world!"

リストの末尾への追加を10回繰り返してみる。

10.times {|i| redis.rpush("tweets", "tweet #{i}") }
=> 10

結果を取得してみる

redis.lrange "tweets", 0, 10
=> ["this is my first tweet.", "hello world!", "tweet 0", "tweet 1", "tweet 2", "tweet 3", "tweet 4", "tweet 5", "tweet 6", "tweet 7", "tweet 8", "tweet 9"]

では、かかった時間を計測しつつ1000回繰り返してみる

st = Time.now; 1000.times {|i| redis.rpush("tweets", "tweet #{i}") }; et = Time.now;

et - st
=> 0.078255

はやw ホントに入ってるかな?

redis.lrange "tweets", 0, 2
=> ["this is my first tweet.", "hello world!", "tweet 0"]

末尾はどうなってるのかな?

redis.lrange "tweets", -3, -1
=> ["tweet 997", "tweet 998", "tweet 999"]

.。+:☆。q(〃・ω・〃)p☆゚イイヨイイヨー

調子に乗って追加で1万個入れてみる。

st = Time.now; 10000.times {|i| redis.rpush("tweets", "tweet #{i}") }; et = Time.now;

et - st
=> 0.755149

1万回繰り返しても1秒以内...


ここでリストの要素数を確認してみる

redis.llen "tweets"
=> 11012

リストの末尾を確認してみる

redis.lrange "tweets", -3, -1
=> ["tweet 9997", "tweet 9998", "tweet 9999"]

ほほー

値の削除

キーを指定して値を削除

redis.del "tweets"
=> 1

現在のリスト長を確認

redis.llen "tweets"
=> 0

構造化されたデータを保存したい
ハッシュをJSONに変換して保存してみよう。

こんなかんじの

tweet = {:id = 1, :user => "shin", :time => Time.now, :body => "hello redis!"}

JSONで保存する

require "json"

キーは"msg_(キー番号)"とします。今回は"msg_1"とかで。

保存

redis.set "msg_1", tweet.to_json
=> "OK"

取得してJSONからRubyのハッシュに変換

JSON.parse(redis.get("msg_1"))
=> {"id"=>1, "user"=>"shin", "time"=>"2011-10-06 16:58:23 +0900", "body"=>"hello redis!"}

すばらしいw

ここまでの仕組みを使ってツイッターのタイムラインのようなモノを実現してみる

実データは"msg_(メッセージ番号)"というキーで保存し、ユーザのタイムラインは"home_(ユーザid)"のようなキーでリストにメッセージのキーを追加していく。

今回は適当にユーザIDが"1"だとしてタイムラインを表現するリストのキーは"home_1"です。
上で作ったツイートのキーを自分のホームのリストに追加します。

redis.rpush "home_1", "msg_1"
=> 1

もう2つほど同じように保存しましょ。

tweet = {:id => 2, 
         :user => "shin", 
         :time => Time.now, 
         :body => "this is second tweet :-)"
        }

redis.set "msg_2", tweet.to_json
=> "OK"

redis.rpush "home_1", "msg_2"=> 2
tweet = {:id => 3,
         :user => "shin",
         :time => Time.now,
         :body => "this is third tweet :-)"
        }

redis.set "msg_3", tweet.to_json
=> "OK"
redis.rpush "home_1", "msg_3"
=> 3
redis.lrange("home_1", -100, -1).map{|key| JSON.parse(redis.get key)}

のように呪文を唱えると...

=> [{"id"=>1, "user"=>"shin", "time"=>"2011-10-06 16:58:23 +0900", "body"=>"hello redis!"}, 
    {"id"=>2, "user"=>"shin", "time"=>"2011-10-06 17:03:28 +0900", "body"=>"this is second tweet :-)"}, 
    {"id"=>3, "user"=>"shin", "time"=>"2011-10-06 17:05:26 +0900", "body"=>"this is third tweet :-)"}]

直近100件(今回は3件しか登録してないから3件)のハッシュデータの配列が得られますね。
Webサイトm等で表示するなら更に.reverseして逆順にしてやればそのまま使えますねw

試しに取得したものを変数に入れて出力してみよう。

home_timeline = redis.lrange("home_1", -100, -1).map{|key| JSON.parse(redis.get key)}.reverse

home_timeline.each do |tweet|
  puts "#{tweet["user"]} | #{tweet["body"]} (#{tweet["time"]})"
end

結果

shin | this is third tweet :-) (2011-10-06 17:05:26 +0900)
shin | this is second tweet :-) (2011-10-06 17:03:28 +0900)
shin | hello redis! (2011-10-06 16:58:23 +0900)

ワンダホ−(๑◕∀◕๑)

...ワンダホ−とか思ったけど、コッチの方が効率がいいですねw

keys = redis.lrange("home_1", -100, -1)
redis.mget(*keys).map{|value| JSON.parse(value)}

最初のやり方だと [キーリストの取得 + キーの数]だけRedisとのやり取りが必要ですが、後の方だと[キーリストの取得 + 値リストの取得]の2回で済みます。



感想
初めてRubyからRedisを操作しましたが、メソッドや引数がRedisのコマンドにそって実装されているので非常に素直な使い心地ですね。解りやすいです。
Redisには上記以外にハッシュやディクショナリなどもありますので、それらも多分Rubyからイケると思われます。

単なるキーバリューストアに比べてRedisはアプリケーション側に片足つっこんできてる印象で、リストなどのデータの扱いが格段にやりやすいです。

上記のツイッター的なものの他にも、例えばスマートフォンから位置を移動するたびに緯度経度を報告させて、それをリストにつっこんでいけば、リスト一つを一回分の移動経路として時刻と一緒に保存して、旅行の移動経路を記録したり、ランニングの移動経路を保存して後からGoogleMap上に表示したり出来ますね+(0゚・∀・) +

それらは全てリレーショナルデータベースでも出来ることですが、負荷的にRedisの方が強そうですし、コーディングする上でもずっと直感的です。

もう少しいろいろと弄ってみて機会があったら実際のアプリケーション開発に使ってみたいですね :-)

次回はErlangからの操作をやってみます。