Map Redis types directly to Ruby objects
Pull request Compare This branch is 382 commits behind nateware:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Redis::Objects - Map Redis types directly to Ruby objects

This is not an ORM. People that are wrapping ORM’s around Redis are missing the point.

The killer feature of Redis is that it allows you to perform atomic operations on individual data structures, like counters, lists, and sets. The atomic part is HUGE. Using an ORM wrapper that retrieves a “record”, updates values, then sends those values back, removes the atomicity, cutting the nuts off the major advantage of Redis. Just use MySQL, k?

This gem provides a Rubyish interface to Redis, by mapping Redis types to Ruby objects, via a thin layer over Ezra's redis gem. It offers several advantages over the lower-level redis-rb API:

  1. Easy to integrate directly with existing ORMs - ActiveRecord, DataMapper, etc. Add counters to your model!

  2. Complex data structures are automatically Marshaled (if you set :marshal => true)

  3. Integers are returned as integers, rather than '17'

  4. Higher-level types are provided, such as Locks, that wrap multiple calls

This gem originally arose out of a need for high-concurrency atomic operations; for a fun rant on the topic, see An Atomic Rant, or scroll down to “Atomicity” in this README.

There are two ways to use Redis::Objects, either as an include in a model class (to integrate with ORMs or other classes), or by using new with the type of data structure you want to create.


gem install redis-objects

Example 1: Model Class Usage

Using Redis::Objects this way makes it trivial to integrate Redis types with an existing ActiveRecord model, DataMapper resource, or other class. Redis::Objects will work with any class that provides an id method that returns a unique value. Redis::Objects will automatically create keys that are unique to each object, in the format:



Redis::Objects needs a handle created by (If you're on Rails, config/initializers/redis.rb is a good place for this.)

require 'redis'
require 'redis/objects'
Redis::Objects.redis = =>, :port => 6379)

Remember you can use Redis::Objects in any Ruby code. There are no dependencies on Rails. Standalone, Sinatra, Resque - no problem.

Model Class

You can include Redis::Objects in any type of class:

class Team < ActiveRecord::Base
  include Redis::Objects

  lock :trade_players, :expiration => 15  # sec
  counter :hits
  counter :runs
  counter :outs
  counter :inning, :start => 1
  list :on_base
  set :outfielders
  value :at_bat

Familiar Ruby array operations Just Work (TM):

@team = Team.find_by_name('New York Yankees')
@team.on_base << 'player1'
@team.on_base << 'player2'
@team.on_base << 'player3'
@team.on_base    # ['player1', 'player2', 'player3']
@team.on_base.length  # 1

Sets work too:

@team.outfielders << 'outfielder1'
@team.outfielders << 'outfielder2'
@team.outfielders << 'outfielder1'   # dup ignored
@team.outfielders  # ['outfielder1', 'outfielder2']
@team.outfielders.each do |player|
  puts player
player = @team.outfielders.detect{|of| of == 'outfielder2'}

And you can do intersections between objects (kinda cool):

@team1.outfielders | @team2.outfielders   # outfielders on both teams
@team1.outfielders & @team2.outfielders   # in baseball, should be empty :-)

Counters can be atomically incremented/decremented (but not assigned):

@team.hits.increment  # or incr
@team.hits.decrement  # or decr
@team.hits.incr(3)    # add 3
@team.runs = 4        # exception

Finally, for free, you get a redis method that points directly to a Redis connection:

@team =

You can use the redis handle to directly call any Redis API command.

Example 2: Standalone Usage

There is a Ruby class that maps to each Redis type, with methods for each Redis API command. Note that calling new does not imply it's actually a “new” value - it just creates a mapping between that object and the corresponding Redis data structure, which may already exist on the redis-server.


Redis::Objects needs a handle to the redis server. For standalone use, you can either set the $redis global variable:

$redis = => 'localhost', :port => 6379)
@list  ='mylist')

Or you can pass the Redis handle into the new method for each type:

redis  = => 'localhost', :port => 6379)
@list  ='mylist', redis)


Create a new counter. The counter_name is the key stored in Redis.

require 'redis/counter'
@counter ='counter_name')
puts @counter.value

This gem provides a clean way to do atomic blocks as well:

@counter.increment do |val|
  raise "Full" if val > MAX_VAL  # rewind counter

See the section on “Atomicity” for cool uses of atomic counter blocks.


A convenience class that wraps the pattern of using setnx to perform locking.

require 'redis/lock'
@lock ='image_resizing', :expiration => 15, :timeout => 0.1)
@lock.lock do
  # do work

This can be especially useful if you're running batch jobs spread across multiple hosts.


Simple values are easy as well:

require 'redis/value'
@value ='value_name')
@value.value = 'a'

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

@account = Account.create!(params[:account])
@newest  ='newest_account', :marshal => true)
@newest.value = @account.attributes
puts @newest.value['username']


Lists work just like Ruby arrays:

require 'redis/list'
@list ='list_name')
@list << 'a'
@list << 'b'
@list.include? 'c'   # false
@list.values  # ['a','b']
@list << 'c'
# etc

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

@list ='list_name', :marshal => true)
@list << {:name => "Nate", :city => "San Diego"}
@list << {:name => "Peter", :city => "Oceanside"}
@list.each do |el|
  puts "#{el[:name]} lives in #{el[:city]}"


Sets work like the Ruby Set class:

require 'redis/set'
@set ='set_name')
@set << 'a'
@set << 'b'
@set << 'a'  # dup ignored
@set.member? 'c'   # false
@set.members  # ['a','b']
@set.each do |member|
  puts member
# etc

You can perform Redis intersections/unions/diffs easily:

@set1 ='set1')
@set2 ='set2')
@set3 ='set3')
members = @set1 & @set2   # intersection
members = @set1 | @set2   # union
members = @set1 + @set2   # union
members = @set1 ^ @set2   # difference
members = @set1 - @set2   # difference
members = @set1.intersection(@set2, @set3)  # multiple
members = @set1.union(@set2, @set3)         # multiple
members = @set1.difference(@set2, @set3)    # multiple

Or store them in Redis:

@set1.interstore('intername', @set2, @set3)
members = @set1.redis.get('intername')
@set1.unionstore('unionname', @set2, @set3)
members = @set1.redis.get('unionname')
@set1.diffstore('diffname', @set2, @set3)
members = @set1.redis.get('diffname')

And use complex data types too, with :marshal => true:

@set1 ='set1', :marshal => true)
@set2 ='set2', :marshal => true)
@set1 << {:name => "Nate", :city => "San Diego"}
@set1 << {:name => "Peter", :city => "Oceanside"}
@set2 << {:name => "Nate", :city => "San Diego"}
@set2 << {:name => "Jeff", :city => "Del Mar"}

@set1 & @set2  # Nate
@set1 - @set2  # Peter
@set1 | @set2  # all 3 people

Sorted Sets

Due to their unique properties, Sorted Sets work like a hybrid between a Hash and an Array. You assign like a Hash, but retrieve like an Array:

require 'redis/sorted_set'
@sorted_set ='number_of_posts')
@sorted_set['Nate']  = 15
@sorted_set['Peter'] = 75
@sorted_set['Jeff']  = 24

# Array access to get sorted order
@sorted_set[0..2]           # => ["Nate", "Jeff", "Peter"]
@sorted_set[0,2]            # => ["Nate", "Jeff"]

@sorted_set['Peter']        # => 75
@sorted_set['Jeff']         # => 24
@sorted_set.score('Jeff')   # same thing (24)

@sorted_set.rank('Peter')   # => 2
@sorted_set.rank('Jeff')    # => 1

@sorted_set.first           # => "Nate"
@sorted_set.last            # => "Peter"
@sorted_set.revrange(0,2)   # => ["Peter", "Jeff", "Nate"]

@sorted_set['Newbie'] = 1
@sorted_set.members         # => ["Newbie", "Nate", "Jeff", "Peter"]

@sorted_set.rangebyscore(10, 100, :limit => 2)   # => ["Nate", "Jeff"]
@sorted_set.members(:withscores => true)         # => [["Newbie", 1], ["Nate", 16], ["Jeff", 28], ["Peter", 76]]

# atomic increment
@sorted_set.incr('Peter')   # shorthand
@sorted_set.incr('Jeff', 4)

The other Redis Sorted Set commands are supported as well; see Sorted Sets API.

Atomic Counters and Locks

You are probably not handling atomicity correctly in your app. For a fun rant on the topic, see An Atomic Rant.

Atomic counters are a good way to handle concurrency:

@team = Team.find(1)
if @team.drafted_players.increment <= @team.max_players
  # do stuff
  @team.team_players.create!(:player_id => 221)
  # reset counter state

Atomic block - a cleaner way to do the above. Exceptions or return nil rewind counter back to previous state:

@team.drafted_players.increment do |val|
  raise Team::TeamFullError if val > @team.max_players
  @team.team_players.create!(:player_id => 221)

Similar approach, using an if block (failure rewinds counter):

@team.drafted_players.increment do |val|
  if val <= @team.max_players
    @team.team_players.create!(:player_id => 221)

Class methods work too - notice we override ActiveRecord counters:

Team.increment_counter :drafted_players, team_id
Team.decrement_counter :drafted_players, team_id, 2
Team.increment_counter :total_online_players  # no ID on global counter

Class-level atomic block (may save a DB fetch depending on your app):

Team.increment_counter(:drafted_players, team_id) do |val|
  TeamPitcher.create!(:team_id => team_id, :pitcher_id => 181)
  Team.increment_counter(:active_players, team_id)

Locks with Redis. On completion or exception the lock is released:

class Team < ActiveRecord::Base
  lock :reorder # declare a lock

@team.reorder_lock.lock do

Class-level lock (same concept)

Team.obtain_lock(:reorder, team_id) do

Lock expiration. Sometimes you want to make sure your locks are cleaned up should the unthinkable happen (server failure). You can set lock expirations to handle this. Expired locks are released by the next process to attempt lock. Just make sure you expiration value is sufficiently large compared to your expected lock time.

class Team < ActiveRecord::Base
  lock :reorder, :expiration => 15.minutes


Copyright © 2009-2010 Nate Wiger. All Rights Reserved. Released under the Artistic License.