redis-objects 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,37 @@
1
+ = Changelog for Redis::Objects
2
+
3
+ == 0.3.0 [Final] (14 April 2010)
4
+
5
+ * Due to Ruby 1.9 bugs and performance considerations, marshaling of
6
+ data types is now OFF by default. You must say :marshal => true for
7
+ any objects that you want serialization enabled on. [Nate Wiger]
8
+
9
+ * Sorted Set class changed slightly due to feedback. You can now get
10
+ an individual element back via @set['item'] since it acts like a Hash.
11
+
12
+ == 0.2.4 [Final] (9 April 2010)*
13
+
14
+ * Added sorted set support via Redis::SortedSet [Nate Wiger]
15
+
16
+ == 0.2.3 [Final] (18 February 2010)*
17
+
18
+ * Added lock expiration to Redis::Lock [Ben VandenBos]
19
+
20
+ * Fixed some bugs [Ben VandenBos]
21
+
22
+ * Added lock tests and test helpers [Ben VandenBos]
23
+
24
+ == 0.2.2 [Final] (14 December 2009)*
25
+
26
+ * Added @set.diff(@set2) with "^" and "-" synonyms (oversight). [Nate Wiger]
27
+
28
+ * Implemented Redis core commands in all data types, such as rename. [Nate Wiger]
29
+
30
+ * Renamed Redis::Serialize to Redis::Helpers::Serialize to keep Redis:: cleaner. [Nate Wiger]
31
+
32
+ * More spec coverage. [Nate Wiger]
33
+
34
+ == 0.2.1 [Final] (27 November 2009)*
35
+
36
+ * First worthwhile public release, with good spec coverage and functionality. [Nate Wiger]
37
+
data/README.rdoc CHANGED
@@ -1,26 +1,31 @@
1
1
  = Redis::Objects - Map Redis types directly to Ruby objects
2
2
 
3
- This is *not* an ORM. People that are wrapping ORM's around Redis are missing
4
- the point.
3
+ This is *not* an ORM. People that are wrapping ORMs around Redis are missing the point.
5
4
 
6
- The killer feature of Redis that it allows you to perform atomic operations
7
- on _individual_ data structures, like counters, lists, and sets. You can then use
8
- these *with* your existing ActiveRecord/DataMapper/etc models, or in classes that have
9
- nothing to do with an ORM or even a database. This gem maps {Redis types}[http://code.google.com/p/redis/wiki/CommandReference]
10
- to Ruby objects, via a thin layer over Ezra's +redis+ gem.
5
+ The killer feature of Redis is that it allows you to perform _atomic_ operations
6
+ on _individual_ data structures, like counters, lists, and sets. The *atomic* part is HUGE.
7
+ Using an ORM wrapper that retrieves a "record", updates values, then sends those values back,
8
+ _removes_ the atomicity, cutting the nuts off the major advantage of Redis. Just use MySQL, k?
9
+
10
+ This gem provides a Rubyish interface to Redis, by mapping {Redis types}[http://code.google.com/p/redis/wiki/CommandReference]
11
+ to Ruby objects, via a thin layer over Ezra's +redis+ gem. It offers several advantages
12
+ over the lower-level redis-rb API:
13
+
14
+ 1. Easy to integrate directly with existing ORMs - ActiveRecord, DataMapper, etc. Add counters to your model!
15
+ 2. Complex data structures are automatically Marshaled (if you set :marshal => true)
16
+ 3. Integers are returned as integers, rather than '17'
17
+ 4. Higher-level types are provided, such as Locks, that wrap multiple calls
11
18
 
12
19
  This gem originally arose out of a need for high-concurrency atomic operations;
13
- for a fun rant on the topic, see
14
- {ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc],
20
+ for a fun rant on the topic, see {An Atomic Rant}[http://nateware.com/2010/02/18/an-atomic-rant],
15
21
  or scroll down to "Atomicity" in this README.
16
22
 
17
- There are two ways to use Redis::Objects, either as an +include+ in a model class,
18
- or by using +new+ with the type of data structure you want to create.
23
+ There are two ways to use Redis::Objects, either as an include in a model class (to
24
+ integrate with ORMs or other classes), or by using new with the type of data structure
25
+ you want to create.
19
26
 
20
27
  == Installation
21
28
 
22
- gem install gemcutter
23
- gem tumble
24
29
  gem install redis-objects
25
30
 
26
31
  == Example 1: Model Class Usage
@@ -29,27 +34,30 @@ Using Redis::Objects this way makes it trivial to integrate Redis types with an
29
34
  existing ActiveRecord model, DataMapper resource, or other class. Redis::Objects
30
35
  will work with _any_ class that provides an +id+ method that returns a unique
31
36
  value. Redis::Objects will automatically create keys that are unique to
32
- each object.
37
+ each object, in the format:
38
+
39
+ model_name:id:field_name
33
40
 
34
41
  === Initialization
35
42
 
36
- Redis::Objects needs a handle created by Redis.new. If you're on Rails,
37
- config/initializers/redis.rb is a good place for this:
43
+ Redis::Objects needs a handle created by Redis.new. (If you're on Rails,
44
+ config/initializers/redis.rb is a good place for this.)
38
45
 
39
46
  require 'redis'
40
47
  require 'redis/objects'
41
48
  Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
42
49
 
43
- You can use Redis::Objects with any framework. There are *no* dependencies on Rails.
44
- I use it from Sinatra and rake tasks all the time.
50
+ Remember you can use Redis::Objects in any Ruby code. There are *no* dependencies
51
+ on Rails. Standalone, Sinatra, Resque - no problem.
45
52
 
46
53
  === Model Class
47
54
 
48
- Include Redis::Objects in any type of class:
55
+ You can include Redis::Objects in any type of class:
49
56
 
50
57
  class Team < ActiveRecord::Base
51
58
  include Redis::Objects
52
59
 
60
+ lock :trade_players, :expiration => 15 # sec
53
61
  counter :hits
54
62
  counter :runs
55
63
  counter :outs
@@ -82,7 +90,7 @@ Sets work too:
82
90
  end
83
91
  player = @team.outfielders.detect{|of| of == 'outfielder2'}
84
92
 
85
- And you can do intersections between ORM objects (kinda cool):
93
+ And you can do intersections between objects (kinda cool):
86
94
 
87
95
  @team1.outfielders | @team2.outfielders # outfielders on both teams
88
96
  @team1.outfielders & @team2.outfielders # in baseball, should be empty :-)
@@ -101,26 +109,28 @@ Finally, for free, you get a +redis+ method that points directly to a Redis conn
101
109
  @team.redis.get('somekey')
102
110
  @team.redis.smembers('someset')
103
111
 
104
- You can use the +redis+ handle to directly call any {Redis command}[http://code.google.com/p/redis/wiki/CommandReference]
112
+ You can use the +redis+ handle to directly call any {Redis API command}[http://code.google.com/p/redis/wiki/CommandReference].
105
113
 
106
114
  == Example 2: Standalone Usage
107
115
 
108
- There is a Ruby object that maps to each Redis type.
116
+ There is a Ruby class that maps to each Redis type, with methods for each
117
+ {Redis API command}[http://code.google.com/p/redis/wiki/CommandReference].
118
+ Note that calling +new+ does not imply it's actually a "new" value - it just
119
+ creates a mapping between that object and the corresponding Redis data structure,
120
+ which may already exist on the redis-server.
109
121
 
110
122
  === Initialization
111
123
 
112
- Again, Redis::Objects needs a handle to the +redis+ server. For standalone use, you
124
+ Redis::Objects needs a handle to the +redis+ server. For standalone use, you
113
125
  can either set the $redis global variable:
114
126
 
115
127
  $redis = Redis.new(:host => 'localhost', :port => 6379)
116
- @value = Redis::Value.new('myvalue')
128
+ @list = Redis::List.new('mylist')
117
129
 
118
130
  Or you can pass the Redis handle into the new method for each type:
119
131
 
120
132
  redis = Redis.new(:host => 'localhost', :port => 6379)
121
- @value = Redis::Value.new('myvalue', redis)
122
-
123
- Your choice.
133
+ @list = Redis::List.new('mylist', redis)
124
134
 
125
135
  === Counters
126
136
 
@@ -140,6 +150,34 @@ This gem provides a clean way to do atomic blocks as well:
140
150
 
141
151
  See the section on "Atomicity" for cool uses of atomic counter blocks.
142
152
 
153
+ === Locks
154
+
155
+ A convenience class that wraps the pattern of {using +setnx+ to perform locking}[http://code.google.com/p/redis/wiki/SetnxCommand].
156
+
157
+ require 'redis/lock'
158
+ @lock = Redis::Lock.new('image_resizing', :expiration => 15, :timeout => 0.1)
159
+ @lock.lock do
160
+ # do work
161
+ end
162
+
163
+ This can be especially useful if you're running batch jobs spread across multiple hosts.
164
+
165
+ === Values
166
+
167
+ Simple values are easy as well:
168
+
169
+ require 'redis/value'
170
+ @value = Redis::Value.new('value_name')
171
+ @value.value = 'a'
172
+ @value.delete
173
+
174
+ Complex data is no problem with :marshal => true:
175
+
176
+ @account = Account.create!(params[:account])
177
+ @newest = Redis::Value.new('newest_account', :marshal => true)
178
+ @newest.value = @account.attributes
179
+ puts @newest.value['username']
180
+
143
181
  === Lists
144
182
 
145
183
  Lists work just like Ruby arrays:
@@ -160,14 +198,15 @@ Lists work just like Ruby arrays:
160
198
  @list.clear
161
199
  # etc
162
200
 
163
- Complex data types are no problem:
201
+ Complex data types are no problem with :marshal => true:
164
202
 
203
+ @list = Redis::List.new('list_name', :marshal => true)
165
204
  @list << {:name => "Nate", :city => "San Diego"}
166
205
  @list << {:name => "Peter", :city => "Oceanside"}
167
206
  @list.each do |el|
168
207
  puts "#{el[:name]} lives in #{el[:city]}"
169
208
  end
170
-
209
+
171
210
  === Sets
172
211
 
173
212
  Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
@@ -208,8 +247,10 @@ Or store them in Redis:
208
247
  @set1.diffstore('diffname', @set2, @set3)
209
248
  members = @set1.redis.get('diffname')
210
249
 
211
- And use complex data types too:
250
+ And use complex data types too, with :marshal => true:
212
251
 
252
+ @set1 = Redis::Set.new('set1', :marshal => true)
253
+ @set2 = Redis::Set.new('set2', :marshal => true)
213
254
  @set1 << {:name => "Nate", :city => "San Diego"}
214
255
  @set1 << {:name => "Peter", :city => "Oceanside"}
215
256
  @set2 << {:name => "Nate", :city => "San Diego"}
@@ -219,26 +260,50 @@ And use complex data types too:
219
260
  @set1 - @set2 # Peter
220
261
  @set1 | @set2 # all 3 people
221
262
 
222
- === Values
263
+ === Sorted Sets
223
264
 
224
- Simple values are easy as well:
265
+ Due to their unique properties, Sorted Sets work like a hybrid between
266
+ a Hash and an Array. You assign like a Hash, but retrieve like an Array:
225
267
 
226
- require 'redis/value'
227
- @value = Redis::Value.new('value_name')
228
- @value.value = 'a'
229
- @value.delete
268
+ require 'redis/sorted_set'
269
+ @sorted_set = Redis::SortedSet.new('number_of_posts')
270
+ @sorted_set['Nate'] = 15
271
+ @sorted_set['Peter'] = 75
272
+ @sorted_set['Jeff'] = 24
230
273
 
231
- Of course complex data is no problem:
274
+ # Array access to get sorted order
275
+ @sorted_set[0..2] # => ["Nate", "Jeff", "Peter"]
276
+ @sorted_set[0,2] # => ["Nate", "Jeff"]
232
277
 
233
- @account = Account.create!(params[:account])
234
- @newest = Redis::Value.new('newest_account')
235
- @newest.value = @account
278
+ @sorted_set['Peter'] # => 75
279
+ @sorted_set['Jeff'] # => 24
280
+ @sorted_set.score('Jeff') # same thing (24)
281
+
282
+ @sorted_set.rank('Peter') # => 2
283
+ @sorted_set.rank('Jeff') # => 1
284
+
285
+ @sorted_set.first # => "Nate"
286
+ @sorted_set.last # => "Peter"
287
+ @sorted_set.revrange(0,2) # => ["Peter", "Jeff", "Nate"]
288
+
289
+ @sorted_set['Newbie'] = 1
290
+ @sorted_set.members # => ["Newbie", "Nate", "Jeff", "Peter"]
291
+
292
+ @sorted_set.rangebyscore(10, 100, :limit => 2) # => ["Nate", "Jeff"]
293
+ @sorted_set.members(:withscores => true) # => [["Newbie", 1], ["Nate", 16], ["Jeff", 28], ["Peter", 76]]
294
+
295
+ # atomic increment
296
+ @sorted_set.increment('Nate')
297
+ @sorted_set.incr('Peter') # shorthand
298
+ @sorted_set.incr('Jeff', 4)
299
+
300
+ The other Redis Sorted Set commands are supported as well; see {Sorted Sets API}[http://code.google.com/p/redis/wiki/SortedSets].
236
301
 
237
302
  == Atomic Counters and Locks
238
303
 
239
304
  You are probably not handling atomicity correctly in your app. For a fun rant
240
305
  on the topic, see
241
- {ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc].
306
+ {An Atomic Rant}[http://nateware.com/2010/02/18/an-atomic-rant].
242
307
 
243
308
  Atomic counters are a good way to handle concurrency:
244
309
 
data/lib/redis/counter.rb CHANGED
@@ -11,10 +11,10 @@ class Redis
11
11
  include Redis::Helpers::CoreCommands
12
12
 
13
13
  attr_reader :key, :options, :redis
14
- def initialize(key, redis=$redis, options={})
14
+ def initialize(key, *args)
15
15
  @key = key
16
- @redis = redis
17
- @options = options
16
+ @options = args.last.is_a?(Hash) ? args.pop : {}
17
+ @redis = args.first || $redis
18
18
  @options[:start] ||= 0
19
19
  @redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
20
20
  end
@@ -25,6 +25,7 @@ class Redis
25
25
  # disconnecting all players).
26
26
  def reset(to=options[:start])
27
27
  redis.set key, to.to_i
28
+ true # hack for redis-rb regression
28
29
  end
29
30
 
30
31
  # Returns the current value of the counter. Normally just calling the
@@ -42,7 +43,7 @@ class Redis
42
43
  # counter will automatically be decremented to its previous value. This
43
44
  # method is aliased as incr() for brevity.
44
45
  def increment(by=1, &block)
45
- val = redis.incr(key, by).to_i
46
+ val = redis.incrby(key, by).to_i
46
47
  block_given? ? rewindable_block(:decrement, val, &block) : val
47
48
  end
48
49
  alias_method :incr, :increment
@@ -53,7 +54,7 @@ class Redis
53
54
  # counter will automatically be incremented to its previous value. This
54
55
  # method is aliased as incr() for brevity.
55
56
  def decrement(by=1, &block)
56
- val = redis.decr(key, by).to_i
57
+ val = redis.decrby(key, by).to_i
57
58
  block_given? ? rewindable_block(:increment, val, &block) : val
58
59
  end
59
60
  alias_method :decr, :decrement
@@ -15,7 +15,7 @@ class Redis
15
15
  def type
16
16
  redis.type key
17
17
  end
18
-
18
+
19
19
  def rename(name, setkey=true)
20
20
  dest = name.is_a?(self.class) ? name.key : name
21
21
  ret = redis.rename key, dest
@@ -41,6 +41,14 @@ class Redis
41
41
  def move(dbindex)
42
42
  redis.move key, dbindex
43
43
  end
44
+
45
+ # See the documentation for SORT: http://code.google.com/p/redis/wiki/SortCommand
46
+ # TODO
47
+ # def sort(options)
48
+ # args = []
49
+ # args += ['sort']
50
+ # from_redis redis.sort key
51
+ # end
44
52
  end
45
53
  end
46
54
  end
@@ -4,6 +4,7 @@ class Redis
4
4
  include Marshal
5
5
 
6
6
  def to_redis(value)
7
+ return value unless options[:marshal]
7
8
  case value
8
9
  when String, Fixnum, Bignum, Float
9
10
  value
@@ -13,6 +14,7 @@ class Redis
13
14
  end
14
15
 
15
16
  def from_redis(value)
17
+ return value unless options[:marshal]
16
18
  case value
17
19
  when Array
18
20
  value.collect{|v| from_redis(v)}
data/lib/redis/list.rb CHANGED
@@ -12,10 +12,10 @@ class Redis
12
12
  include Redis::Helpers::Serialize
13
13
 
14
14
  attr_reader :key, :options, :redis
15
- def initialize(key, redis=$redis, options={})
15
+ def initialize(key, *args)
16
16
  @key = key
17
- @redis = redis
18
- @options = options
17
+ @options = args.last.is_a?(Hash) ? args.pop : {}
18
+ @redis = args.first || $redis
19
19
  end
20
20
 
21
21
  # Works like push. Can chain together: list << 'a' << 'b'
@@ -62,7 +62,7 @@ class Redis
62
62
  at(index)
63
63
  end
64
64
  end
65
-
65
+
66
66
  # Delete the element(s) from the list that match name. If count is specified,
67
67
  # only the first-N (if positive) or last-N (if negative) will be removed.
68
68
  # Use .del to completely delete the entire key.
data/lib/redis/lock.rb CHANGED
@@ -10,11 +10,12 @@ class Redis
10
10
  class LockTimeout < StandardError; end #:nodoc:
11
11
 
12
12
  attr_reader :key, :options, :redis
13
- def initialize(key, redis=$redis, options={})
13
+ def initialize(key, *args)
14
14
  @key = key
15
- @redis = redis
16
- @options = options
15
+ @options = args.last.is_a?(Hash) ? args.pop : {}
16
+ @redis = args.first || $redis
17
17
  @options[:timeout] ||= 5
18
+ @options[:init] = false if @options[:init].nil? # default :init to false
18
19
  @redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
19
20
  end
20
21
 
@@ -56,7 +56,7 @@ class Redis
56
56
  def increment_counter(name, id=nil, by=1, &block)
57
57
  verify_counter_defined!(name, id)
58
58
  initialize_counter!(name, id)
59
- value = redis.incr(field_key(name, id), by).to_i
59
+ value = redis.incrby(field_key(name, id), by).to_i
60
60
  block_given? ? rewindable_block(:decrement_counter, name, id, value, &block) : value
61
61
  end
62
62
 
@@ -65,7 +65,7 @@ class Redis
65
65
  def decrement_counter(name, id=nil, by=1, &block)
66
66
  verify_counter_defined!(name, id)
67
67
  initialize_counter!(name, id)
68
- value = redis.decr(field_key(name, id), by).to_i
68
+ value = redis.decrby(field_key(name, id), by).to_i
69
69
  block_given? ? rewindable_block(:increment_counter, name, id, value, &block) : value
70
70
  end
71
71
 
@@ -73,7 +73,8 @@ class Redis
73
73
  def reset_counter(name, id=nil, to=nil)
74
74
  verify_counter_defined!(name, id)
75
75
  to = @redis_objects[name][:start] if to.nil?
76
- redis.set(field_key(name, id), to)
76
+ redis.set(field_key(name, id), to.to_i)
77
+ true
77
78
  end
78
79
 
79
80
  private
@@ -16,7 +16,6 @@ class Redis
16
16
  # so it can be used alongside ActiveRecord/DataMapper, etc.
17
17
  def lock(name, options={})
18
18
  options[:timeout] ||= 5 # seconds
19
- options[:init] = false if options[:init].nil? # default :init to false
20
19
  @redis_objects[name] = options.merge(:type => :lock)
21
20
  if options[:global]
22
21
  instance_eval <<-EndMethods
@@ -36,9 +35,6 @@ class Redis
36
35
  end
37
36
  EndMethods
38
37
  end
39
-
40
-
41
-
42
38
  end
43
39
 
44
40
  # Obtain a lock, and execute the block synchronously. Any other code
@@ -0,0 +1,45 @@
1
+ # This is the class loader, for use as "include Redis::Objects::Sets"
2
+ # For the object itself, see "Redis::Set"
3
+ require 'redis/sorted_set'
4
+ class Redis
5
+ module Objects
6
+ module SortedSets
7
+ def self.included(klass)
8
+ klass.send :include, InstanceMethods
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ # Class methods that appear in your class when you include Redis::Objects.
13
+ module ClassMethods
14
+ # Define a new list. It will function like a regular instance
15
+ # method, so it can be used alongside ActiveRecord, DataMapper, etc.
16
+ def sorted_set(name, options={})
17
+ @redis_objects[name] = options.merge(:type => :sorted_set)
18
+ if options[:global]
19
+ instance_eval <<-EndMethods
20
+ def #{name}
21
+ @#{name} ||= Redis::SortedSet.new(field_key(:#{name}, ''), redis, @redis_objects[:#{name}])
22
+ end
23
+ EndMethods
24
+ class_eval <<-EndMethods
25
+ def #{name}
26
+ self.class.#{name}
27
+ end
28
+ EndMethods
29
+ else
30
+ class_eval <<-EndMethods
31
+ def #{name}
32
+ @#{name} ||= Redis::SortedSet.new(field_key(:#{name}), redis, self.class.redis_objects[:#{name}])
33
+ end
34
+ EndMethods
35
+ end
36
+
37
+ end
38
+ end
39
+
40
+ # Instance methods that appear in your class when you include Redis::Objects.
41
+ module InstanceMethods
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/redis/objects.rb CHANGED
@@ -6,7 +6,7 @@ class Redis
6
6
  # Redis::Objects enables high-performance atomic operations in your app
7
7
  # by leveraging the atomic features of the Redis server. To use Redis::Objects,
8
8
  # first include it in any class you want. (This example uses an ActiveRecord
9
- # subclass, but that is *not* required.) Then, use +counter+ and +lock+
9
+ # subclass, but that is *not* required.) Then, use +counter+, +lock+, +set+, etc
10
10
  # to define your primitives:
11
11
  #
12
12
  # class Game < ActiveRecord::Base
@@ -14,8 +14,8 @@ class Redis
14
14
  #
15
15
  # counter :joined_players
16
16
  # counter :active_players
17
- # set :player_ids
18
17
  # lock :archive_game
18
+ # set :player_ids
19
19
  # end
20
20
  #
21
21
  # The, you can use these counters both for bookeeping and as atomic actions:
@@ -39,10 +39,11 @@ class Redis
39
39
  dir = File.expand_path(__FILE__.sub(/\.rb$/,''))
40
40
 
41
41
  autoload :Counters, File.join(dir, 'counters')
42
- autoload :Values, File.join(dir, 'values')
43
42
  autoload :Lists, File.join(dir, 'lists')
44
- autoload :Sets, File.join(dir, 'sets')
45
43
  autoload :Locks, File.join(dir, 'locks')
44
+ autoload :Sets, File.join(dir, 'sets')
45
+ autoload :SortedSets, File.join(dir, 'sorted_sets')
46
+ autoload :Values, File.join(dir, 'values')
46
47
 
47
48
  class NotConnected < StandardError; end
48
49
 
@@ -61,10 +62,11 @@ class Redis
61
62
 
62
63
  # Pull in each object type
63
64
  klass.send :include, Redis::Objects::Counters
64
- klass.send :include, Redis::Objects::Values
65
65
  klass.send :include, Redis::Objects::Lists
66
- klass.send :include, Redis::Objects::Sets
67
66
  klass.send :include, Redis::Objects::Locks
67
+ klass.send :include, Redis::Objects::Sets
68
+ klass.send :include, Redis::Objects::SortedSets
69
+ klass.send :include, Redis::Objects::Values
68
70
  end
69
71
  end
70
72
 
data/lib/redis/set.rb CHANGED
@@ -13,10 +13,10 @@ class Redis
13
13
  attr_reader :key, :options, :redis
14
14
 
15
15
  # Create a new Set.
16
- def initialize(key, redis=$redis, options={})
16
+ def initialize(key, *args)
17
17
  @key = key
18
- @redis = redis
19
- @options = options
18
+ @options = args.last.is_a?(Hash) ? args.pop : {}
19
+ @redis = args.first || $redis
20
20
  end
21
21
 
22
22
  # Works like add. Can chain together: list << 'a' << 'b'