カカリアスタジオブログ

Happy Elementsのゲームブランド「カカリアスタジオ」の公式ブログです。

redis-objectsがなくてもプロジェクトは回るが

あるととても便利だと思います

はじめに

エンジニアの@ryooo321です。
よろしくお願いします。
ご存知の方も多いかもしれませんが、今回はRubyからRedisを使う際にとても扱いやすいredis-objectsをご紹介したいと思います。

https://github.com/nateware/redis-objects


特徴

・ORMではない

・Redisのデータ型(counter, value, list, hash, set, sorted-set)をサポート

・ロック機能も利用可能



好きなところ

・モデルの任意のプロパティをRedisに保存するような感覚で利用できる点。

・Redisのキー情報をシームレスに管理できる点。

・ARモデルの一部プロパティをRedisに移したりできるので、データストア単位でなく理想の単位でモデルを定義できる点。


This is not an ORM. People that are wrapping ORM’s around Redis are missing the point.
(readmeより引用)
作者の方はこのように言っており、本当にとても使いやすい形になっております。


目次

1. Counter
2. List
3. Set
4. Hash
5. Sorted Set
6. global
7. Lock
8. redisオブジェクト



1. Counter

数値をアトミックにincrement/decrementする型です。

class User < ActiveRecord::Base
  include Redis::Objects
  counter :friend_count, :start => 0
  # startは省略可
end

@user = User.find(id)

# インクリメント
@user.friend_count.increment

# 渡したブロックで例外発生もしくはnilが帰ったときは、decrementを発行して数値を戻す
@user.friend_count.increment do |new_value|
  raise 'friend count is limited by 20' if 20 < new_value
  true
end

# Userを取得せずにアクセス
friend_count = User.get_counter(:friend_count, user_id)
User.increment_counter(:friend_count, user_id)
Userを取得せずにアクセスできる点、オブジェクト経由で操作する時はuser_idを渡さないでよい点(キーがシームレス)が便利です。



2. List

配列をアトミックに操作できる型です。

class User < ActiveRecord::Base
  include Redis::Objects
  list :friend_ids, :maxlength => 20
  # startは省略可
end

@user = User.find(id)

# rubyの配列のように使えます(Enumerable)
# 追加
@user.friend_ids << 123
@user.friend_ids.push(123)

# 変更
@user.friend_ids[2] = 123

# 削除
@user.friend_ids.delete(123)

# 取得
@user.friend_ids[2]
@user.friend_ids[2..4]
@user.friend_ids.at(2)
@user.friend_ids.first
@user.friend_ids.each do |user_id|
  # hoge
end
@user.friend_ids.values

Userを取得せずに操作するメソッドは現在ありません。

しかし、⬇⬇⬇このようにしてUserオブジェクトなしにアクセスできます。

name = :friend_ids
friend_ids = Redis::List.new(User.redis_field_key(name, user_id), User.redis, User.redis_objects[name])
friend_ids.values



3. Set

重複無効な配列をアトミックに操作できる型です。

class User < ActiveRecord::Base
  include Redis::Objects
  set :friend_ids
end

@user = User.find(id)

# rubyの配列のように使えます(Enumerable)
@user.friend_ids << 123
@user.friend_ids.each do |user_id|
  # hoge
end

# setなので、重複したものは無視
@user.friend_ids << 123
@user.friend_ids << 123 # 2回目は無視
@user.friend_ids.member?(me.id)

# 共通の友達
@user.friend_ids & me.friend_ids

# どちらかの友達
@user.friend_ids | me.friend_ids
@user.friend_ids + me.friend_ids

# 共通でない友達(差分)
@user.friend_ids ^ me.friend_ids
@user.friend_ids - me.friend_ids

# Userを取得せずに操作するメソッドは現在ありませんが、List同様(前述)に取得できます



4. Hash

ハッシュをアトミックに操作できる型です。

class User < ActiveRecord::Base
  include Redis::Objects
  hash_key :item_count_map
end

@user = User.find(id)

# rubyのHashのように使えます(Enumerable)
@user.item_count_map[item_id] = 10
@user.item_count_map.each do |item_id, count|
  # hoge
end

# まとめて操作
@user.item_count_map.bulk_set(:a => 10, :b => 20, :c => 30)
@user.item_count_map.bulk_get(:a, :b)
# => {:a => 10, :b => 20}
@user.item_count_map.bulk_values(:a, :b)
# => [10, 20]

# 個別にインクリメント
@user.item_count_map.incr(:a, 50)
@user.item_count_map[:a]
# => 60

# Userを取得せずに操作するメソッドは現在ありませんが、List同様(前述)に取得できます



5. Sorted Set

ソート済みハッシュをアトミックに操作できる型です。

class User < ActiveRecord::Base
  include Redis::Objects
  sorted_set :article_rate
end

@user = User.find(id)

# articleごとに評価スコアを登録
@user.article_rate[:a] = 30
@user.article_rate[:b] = 50
@user.article_rate[:c] = 10

@user.article_rate.score(:c)
# => 10

# 順位
# 昇順
@user.article_rate.rank(:b)
# => 2

# 降順
@user.article_rate.revrank(:b)
# => 0

# スコアの範囲取得
@user.article_rate.rangebyscore(0, 100, :limit => 1)
# => [:c]
@user.article_rate.members(:with_scores => true)
# => [[:c, 10], [:a, 30], [:b, 50]]

# atomicなインクリメント
@user.article_rate.incr(:c, 100)
@user.article_rate.score(:c)
# => 110

# Userを取得せずに操作するメソッドは現在ありませんが、List同様(前述)に取得できます



6. global

すべてのデータ型クラスでglobalオプションが使えます。

trueを指定すると、クラス単位でredisキーが共通になります。

※ 同モデルのオブジェクトすべてが、同じキャッシュを見るような状態です。

class User < ActiveRecord::Base
  include Redis::Objects
  sorted_set :article_rate
  sorted_set :event_point, :global => true
end

# これは@userごとのランキング
@user.article_rate.rank(:a)

# これは全ユーザーのランキング
@user.event_point.rank(:a)



7. Lock

lockの実装としてはredisにフラグ値をsetし、そのフラグがある場合は処理を待ち合わせる作りです。

同じhoge_lockを使っている箇所とで排他制御にできます。

class User < ActiveRecord::Base
  include Redis::Objects
  lock :hoge, :expiration => 20.second, :timeout => 1.second
  
  # lockのオプション
  # timeout :
  #    指定された時間以上にロック解除を待った場合、例外を投げます。
  # expiration :
  #    万が一ロックが解除されない状態になっても、指定された時間が経つとredis側でロック解除します。
end

@user = User.find(id)

# 処理Aと処理Bは排他的に動く
@user.hoge_lock.lock do
  # 処理A
end
@user.hoge_lock.lock do
  # 処理B
end



8. redisオブジェクト

redisオブジェクトはredis gemのオブジェクトで、redis-objectsで未実装のAPIもredisオブジェクト経由で呼び出せる場合があります。

class User < ActiveRecord::Base
  include Redis::Objects
  value :hoge
end

@user = User.find(id)

# redisオブジェクトへは、下記のようにアクセスできます。
redis = User.redis
redis = @user.redis

redis.pipelined do
  redis.set "foo", "bar"
  redis.incr "baz"
end



おわりに

本稿にお付き合い下さいましてありがとうございました。

この素晴らしいプロダクトを、みなさまが少しでもよいと思っていただければ幸いです。



一緒に働きたい方、絶賛 募集中

京都でスキルアップしたいエンジニアの皆さん、ご応募お待ちしています!
社内にはプロジェクターが使えるバースペースがあり、業務後のコミュニケーションの場となっています。

京都でスキルアップしたい学生さん、アルバイトも可能なのでご応募お待ちしています!
オフィスワークでドリンク飲み放題、時給は高く、シフトの自由度も高いです。

大阪、滋賀、神戸から通勤実績あります
イラストレーターさん、シナリオライターさんも募集中(アルバイト可)です!


© Happy Elements K.K