RubyでRedisしてみよう(個人的まとめ)

herokuで永続化させるのにRedis To Goを使おうと思っているので、Rubyから扱えるRedis環境を作ってみます。
そして、オブジェクト指向でRedisを扱えるredis-objects gemをインストールします。

まずRedisインストール

  • 日本語解説

redisドキュメント日本語訳 — redis 2.0.3 documentation

  • Redis簡易解説本

the-little-redis-book/redis.md at master · craftgear/the-little-redis-book · GitHub

  • ドットインストール

http://dotinstall.com/lessons/basic_redis

$ brew install redis
==> Downloading http://download.redis.io/releases/redis-2.6.16.tar.gz
######################################################################## 100.0%
==> make -C /private/tmp/redis-Tw4H/redis-2.6.16/src CC=cc
==> Caveats
To have launchd start redis at login:
    ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents
Then to load redis now:
    launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
Or, if you don't want/need launchctl, you can just run:
    redis-server /usr/local/etc/redis.conf
==> Summary
/usr/local/Cellar/redis/2.6.16: 10 files, 1.3M, built in 36 seconds
  • 初期設定ファイルの場所

/usr/local/etc/redis.conf

# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
# 
# The Append Only File will also be created inside this directory.
# 
# Note that you must specify a directory here, not a file name.
dir /usr/local/var/db/redis/

…ですが、redis-serverコマンドを実行したディレクトリが実行ディレクトリになるようで、そこにdump.rdbファイルが保存されていましたw

実行
  • サーバ
$ which redis-server
/usr/local/bin/redis-server
$ redis-
redis-benchmark   redis-check-dump  redis-sentinel    
redis-check-aof   redis-cli         redis-server      
$ redis-server 
[316] 01 Oct 18:34:13.904 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
[316] 01 Oct 18:34:13.907 * Max number of open files set to 10032
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 2.6.16 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in stand alone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 316
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

[316] 01 Oct 18:34:13.909 # Server started, Redis version 2.6.16
[316] 01 Oct 18:34:13.909 * The server is now ready to accept connections on port 6379
[316] 01 Oct 19:34:14.001 * 1 changes in 3600 seconds. Saving...
[316] 01 Oct 19:34:14.002 * Background saving started by pid 450
[450] 01 Oct 19:34:14.005 * DB saved on disk
[316] 01 Oct 19:34:14.104 * Background saving terminated with success
  • クライアント
$ redis-cli
redis 127.0.0.1:6379>

サーバを再起動してもデータベースはちゃんと残っていました。

  • Redis to Goへredis-cliで繋ぐ
$ redis-cli -h my-host -p port -a mypassword
Redis To Go

の形式です。
URLだと

//:@:/
Uniform Resource Locator - Wikipedia

なのでRedis To Go | Heroku Dev Centerに記載されている例の

redis://redistogo:44ec0bc04dd4a5afe77a649acee7a8f3@drum.redistogo.com:9092/

へ繋ぐ*1場合には

$ redis-cli -h drum.redistogo.com -p 9092 -a 44ec0bc04dd4a5afe77a649acee7a8f3

と実行します。

  • データベースを削除する
redis 127.0.0.1:6379> flushdb
OK

ヘルプ参照してみると

redis 127.0.0.1:6379> help FLUSHDB

  FLUSHDB -
  summary: Remove all keys from the current database
  since: 1.0.0
  group: server

redis 127.0.0.1:6379> help FLUSHALL

  FLUSHALL -
  summary: Remove all keys from all databases
  since: 1.0.0
  group: server

ガラガラポンにはFLUSHALL。引数無し。覚えた。

redis-objects gemインストール

  • ソースなど

GitHub - nateware/redis-objects: Map Redis types directly to Ruby objects

  • マニュアル

File: README — Documentation for redis-objects (1.4.3)

  • インストール
$ gem install redis-objects
Fetching: redis-3.0.4.gem (100%)
Fetching: redis-objects-0.7.0.gem (100%)
Successfully installed redis-3.0.4
Successfully installed redis-objects-0.7.0
2 gems installed
redis-objectsと一緒に入ったredis-rb gem
  • ソースなど

GitHub - redis/redis-rb: A Ruby client library for Redis

  • マニュアル

File: README — Documentation for redis (4.0.3)

Redis To Go

  • インストール
$ heroku addons:add redistogo:nano
Adding redistogo:nano on heroku-app... done, v7 (free)
Use `heroku addons:docs redistogo` to view documentation.
  • マニュアル(各言語における設定)

Redis To Go | Heroku Dev Center

redis-objectsのポイント

マニュアル File: README — Documentation for redis-objects (1.4.3) に従って確認していきます。
今回はOption 2のスタンドアロン利用のほうだけを見ます。
もちろん

require 'redis-objects'

の宣言が必要です。そして上記URLならばRedis.currentの宣言として

redis_url = "redis://redistogo:44ec0bc04dd4a5afe77a649acee7a8f3@drum.redistogo.com:9092/"
Redis.current = Redis.new(url: redis_url)

を行います。もしherokuでRedis To Goを使っている場合には

redis_url = ENV['REDISTOGO_URL']
Redis.current = Redis.new(url: redis_url)

で済みます。

Redis::TimeoutError を避けるためにタイムアウトを延ばす

Redis.new の引数として :timeout に float を渡します。初期値は5.0(秒)です。

Redisキー

Redisのインスタンスを作る初期化メソッド Redis::Value.new、Redis::List.new、Redis::HashKey.new…どれであっても第一引数はRedisキーです。RedisではこのRedisキーにより各データベースを区別します。
Redisに登録されているRedisキーは

Redis.current.keys

により配列で返されます。

文字列型
  • :marshal => trueにすると、値にHashなどのオブジェクトを入れることができる。

おそらく例示されているのはActiveRecordsだと思いますが、AR知らないので分からないですw
でも値としてオブジェクトを取り扱えているのは分かります。

Complex data is no problem with :marshal => true:

> @account = Account.create!(params[:account])
> @newest  = Redis::Value.new('newest_account', :marshal => true)
> @newest.value = @account.attributes
> puts @newest.value['username']
リスト型
  • 基本的には Ruby の配列と同じです。
  • ただし範囲外のインデックスに要素を入れようとすると Redis::CommandError: ERR index out of range エラーになります>_<。
  • シンボルも数値も全て基本的にString。詳しくはハッシュ型のところで解説。
  • 配列をリスト型へコピーしたい場合

リスト型インスタンスを一旦リセットしてからぶち込む。

> @list.clear
> @list.push(*array)
  • :marshal => trueにすると、リストの値にHashを入れるなどの複雑なことが出来る。

Complex data types are no handled with :marshal => true:

> @list = Redis::List.new('list_name', :marshal => true)
> @list << {:name => "Nate", :city => "San Diego"}  # Hash
> @list << {:name => "Peter", :city => "Oceanside"} # Hash
> @list.each do |el|
>   puts "#{el[:name]} lives in #{el[:city]}"
> end
# => Nate lives in San Diego
# => Peter lives in Oceanside
> @list.values
# => [{:name=>"Nate", :city=>"San Diego"}, {:name=>"Peter", :city=>"Oceanside"}]
  • :marshal => trueが無い場合
> @list = Redis::List.new('list_name')
> @list << {:name => "Nate", :city => "San Diego"}  # String
> @list << {:name => "Peter", :city => "Oceanside"} # String
> @list.each do |el|
>   puts "#{el[:name]} lives in #{el[:city]}"
> end
# => TypeError: can't convert Symbol into Integer
> @list.values
# => ["{:name=>\"Nate\", :city=>\"San Diego\"}", "{:name=>\"Peter\", :city=>\"Oceanside\"}"]
ハッシュ型
  • HashはRedis::HashKey.new。Redis::Hash.newじゃないので注意。
  • RubyのHashは当然ながら {key => value} と呼ぶのだけど、Redisではデータベースの「キー」が「key」なので {field => value} と呼ぶことに注意
@hash = Redis::HashKey.new('hash_name') <= hash_nameが「キー」

とはいうものの、ハッシュのキー(つまり"field")全てを出力するメソッドは

@hash.keys

だったりする…ややこしい。
いや@hash.fieldsなんてメソッドをつくられるほうがいやだけど。

なお、内容の全表示は

@hash.all

です。

  • Rubyのハッシュreplace_hashをハッシュ型へコピーしたい場合

ハッシュ型インスタンスを一旦リセットしてからぶち込む。

> @hash.clear
> @hash.update(replace_hash) # もしくは @hash.bulk_set(replace_hash)
  • (Hashに限らないけど)シンボルも数値も全て基本的にString

Redisリスト型、Redisセット型、Redisソート済みセット型、Redisハッシュ型で保持される各要素はRedis文字列型であることを覚えておいてください。
文字列型 — redis 2.0.3 documentation

なので:aも"a"も同じですし、1も"1"も同じ。

> @hash[:a] = 1
# => 1
> @hash[:b] = 2
# => 2
> @hash[:c] = 3
# => 3
> @hash.each{ |k,v| puts "#{k} = #{v}" }
a = 1
b = 2
c = 3
# => {"a"=>"1", "b"=>"2", "c"=>"3"}
> @hash["a"]
# => "1"
    • increment/decrementも使えるが、数値がStringに変換されて保存されているので、メソッドの使い方に注意する必要がある
> @hash[:c]
# => "3"
> @hash[:c].incr(6)
NoMethodError: undefined method `incr' for "3":String
> @hash.incr(:c, 6)
# => 9
  • :marshal => trueにすると、値にHashを入れるなどの複雑なことが出来る
  • 値に含められたハッシュは、本当のハッシュなので、シンボルを使った場合に文字列扱いされないことに注意
> hh = Redis::HashKey.new("hh")
# => #<Redis::HashKey:0x000001008e2640>
> hh[:a] = "a"
# => "a"
> hh[:c] = {cc: "CC"}
# => {:cc=>"CC"}
> hh.all
# => {"a"=>"a", "c"=>"{:cc=>\"CC\"}"} #フィールド "c" に対応するのは単なる文字列 "{:cc=>\"CC\"}"
> hh[:a]
# => "a"
> hh["a"]
# => "a"
> hh[:c][:cc] #文字列 "{:cc=>\"CC\"}" に対して String#[] メソッドを適用しようとした
TypeError: can't convert Symbol into Integer #が、カギ括弧の中がシンボル :cc だったのでエラー
> hhh = Redis::HashKey.new("hhh", :marshal => true)
# => #<Redis::HashKey:0x00000100c4c268>
> hhh[:a] = "a"
# => "a"
> hhh[:c] = {cc: "CC"}
# => {:cc=>"CC"}
> hhh.all
# => {"a"=>"a", "c"=>{:cc=>"CC"}}
> hhh[:a]
# => "a"
> hhh[:c]
# => {:cc=>"CC"}
> hhh[:c][:cc]
# => "CC"
> hhh["c"][:cc] # シンボル:cは文字列"c"と同等
# => "CC"
> hhh["c"]["cc"] # シンボル:ccは文字列"cc"と同等にならない
# => nil

# 二次元配列的に書き換えたい場合
> hhh["c"]["cc"] = "CCC" # こんな感じでいけるよねRubyオンリーならば。
# => "CCC"
> hhh["c"]["cc"] # しかし値を確認すると…
# => "CC"        # 書き換わっていない >_<
> hhh.all
# => {"a"=>"a", "c"=>{:cc=>"CC"}} # Redis側を確認すると元のまま。
# 単にローカルのハッシュ {:cc=>"CC"} に対して一時的に {:cc=>"CCC"} と書き直しただけになってしまっていた。
# ということで。
> hhh["c"] = hhh["c"].update({cc: "CCC"}) # field "c" に対するハッシュを変更してやる必要がある。
# => {:cc=>"CCC"}
> hhh.all
# => {"a"=>"a", "c"=>{:cc=>"CCC"}}
  • さらに注意:(繰り返しますが)Redisキーが同じ場合、おなじデータを指している(参照している)ことになるので、オブジェクトは異なっても本質的に等しい。
> a  = Redis::HashKey.new('alpha', :marshal => true)
# => #<Redis::HashKey:0x00000101b9cf38>
> aa = Redis::HashKey.new('alpha', :marshal => true)
# => #<Redis::HashKey:0x00000101bb4f20>
> a[:a] = 0
# => 0
> a[:b] = 1
# => 1
> a.all
# => {"a"=>"0", "b"=>"1"}
> aa.all
# => {"a"=>"0", "b"=>"1"}
> a == aa
# => false
> a.all == aa.all
# => true
Set型、SortedSet型、Counter、Lock

略。

データ削除

Redisキーを指定した削除

例えばredis_keyという名のキーを削除したい(つまりredis_keyに結びついたデータベースを削除したい)場合には

Redis.current.del("redis_key")

で消せます。なお複数削除したい場合には、キーの配列を引数に渡せばOKです。

リスト型
  • 値を指定しての削除

値が複数記憶されている場合は、その全てが削除されて、その個数が返される。

@list = Redis::List.new('list_name', :marshal => true)
@list << "a"
@list << "bb"
@list << "ccc"
@list.to_a
# => ["a", "bb", "ccc"]
@list.delete("a")
# => 1
@list << "dddd"
@list << "dddd"
@list << "dddd"
@list.to_a
# => ["bb", "ccc", "dddd", "dddd", "dddd"]
@list.delete("dddd")
# => 3
@list.to_a
# => ["bb", "ccc"]
    • Redis::Listオブジェクト内容全ての削除

どちらも同じです。

@list.clear
@list.del
      • 細かい解説

Redis::Helpers::CoreCommands#deleteメソッドは「オブジェクト内容全ての削除」をします。Redis::Helpers::CoreCommands#clearRedis::Helpers::CoreCommands#delはそのエイリアスです。
Redis::Helpers::CoreCommandsモジュールは、それぞれの型のクラスでインクルードされていますので、基本的にはどの型でも同じメソッド(ここでは #delete)が使えます。ただ、リスト型ではRedis::List#deleteが「値指定の削除メソッド」として上書き設定されているので、「オブジェクト内容全ての削除」をしたい場合にはRedis::List#clearまたはRedis::List#delを使います。

ハッシュ型
  • 特定キー(特定"field")の削除
@hash.delete(:a)

上記した、Redis::Listクラスでの「細かい解説」と同様に、ハッシュ型でもRedis::HashKey#deleteが「値指定の削除メソッド」として上書き設定されています。

    • Redis::HashKeyオブジェクト内容全ての削除
@hash.clear
全体削除(ガラガラポン

Redisの中身全てを削除したい場合は

Redis.current.flushall

Method: Redis#flushall — Documentation for redis (4.0.3)

ディスクへ即時保存させるには

コマンドリファレンス:BGSAVE():永続化処理コマンド:制御コマンド - redis 2.0.3 documentation
redis-objects gemには対応コマンドが無いのでredis-rb gemのコマンドを使います。

Redis.new.bgsave

接続を切る

インスタンス#quit使えば良いようです。

Redis#quit

Close the connection.
Method: Redis#quit — Documentation for redis (4.0.3)

「コネクション多すぎ」エラーが出るときには、利用していないコネクションを切ればいいと思います。

*1:繋がりませんw