redis-objects 1.7.0 → 2.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.rdoc +12 -0
- data/README.md +245 -142
- data/lib/redis/list.rb +2 -10
- data/lib/redis/objects/locks.rb +1 -1
- data/lib/redis/objects/version.rb +1 -1
- data/lib/redis/objects.rb +125 -7
- data/lib/redis/set.rb +2 -1
- data/lib/redis/sorted_set.rb +2 -2
- data/redis-objects.gemspec +1 -1
- data/spec/redis_key_naming_spec.rb +427 -0
- data/spec/redis_objects_active_record_spec.rb +1 -1
- data/spec/redis_objects_conn_spec.rb +4 -14
- data/spec/redis_objects_instance_spec.rb +41 -28
- data/spec/redis_objects_model_spec.rb +42 -16
- data/spec/spec_helper.rb +4 -3
- metadata +9 -10
data/README.md
CHANGED
|
@@ -1,22 +1,82 @@
|
|
|
1
|
-
Redis::Objects - Map Redis types directly to Ruby objects
|
|
2
|
-
=========================================================
|
|
1
|
+
# Redis::Objects - Map Redis types directly to Ruby objects
|
|
3
2
|
|
|
4
3
|
[](https://travis-ci.com/github/nateware/redis-objects)
|
|
5
4
|
[](https://codecov.io/gh/nateware/redis-objects)
|
|
6
5
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MJF7JU5M7F8VL)
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
## Important 2.0 changes
|
|
8
|
+
|
|
9
|
+
Several longstanding bugs have been addressed in this release. However, this may require some
|
|
10
|
+
code changes as part of the upgrade.
|
|
11
|
+
|
|
12
|
+
### Renaming of `lock` Method
|
|
13
|
+
|
|
14
|
+
The `lock` method that collided with `ActiveRecord::Base` has been renamed `redis_lock`.
|
|
15
|
+
This means your classes need to be updated to call `redis_lock` instead:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
class YouClassNameHere < ActiveRecord::Base
|
|
19
|
+
include Redis::Objects
|
|
20
|
+
redis_lock :mylock # formerly just "lock"
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For more details on the issue and fix refer to [#196](https://github.com/nateware/redis-objects/issues/196).
|
|
25
|
+
|
|
26
|
+
### New key naming method
|
|
27
|
+
|
|
28
|
+
A new method to determine the internal key naming scheme has been added to fix a
|
|
29
|
+
longstanding bug. (Refer to [#213](https://github.com/nateware/redis-objects/issues/231))
|
|
30
|
+
By default, backwards compatibility is maintained, but this means that in some cases,
|
|
31
|
+
Nested classes (for example `Dog::Behavior` and `Cat::Behavior`) would have key names
|
|
32
|
+
that collide (they would both start with `Behavior` as anything before `::` is stripped).
|
|
33
|
+
|
|
34
|
+
To ensure names are unique, you can set `Redis::Objects.prefix_style = :modern` after
|
|
35
|
+
you load the module. By default, it is set to `Redis::Objects.prefix_style = :legacy`.
|
|
36
|
+
|
|
37
|
+
### Migrating old keys
|
|
38
|
+
|
|
39
|
+
If your Redis::Object subclasses are nested such as `Dog::Behavior` and `Cat::Behavior`,
|
|
40
|
+
then you should upgrade them using the below proceedure. (only needed once)
|
|
41
|
+
|
|
42
|
+
Create a script like this:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class Dog::Behavior < ActiveRecord::Base
|
|
46
|
+
include Redis::Objects
|
|
47
|
+
# ... your relevant redis_object definitions here (counters/sets) ...
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Dog::Behavior.migrate_redis_legacy_keys
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You need to find a time when you can temporarily pause writes to your redis server
|
|
54
|
+
so that you can run that script. It uses `redis.scan` internally so it should be able to
|
|
55
|
+
handle a high number of keys. For large data sets, it could take a while.
|
|
56
|
+
|
|
57
|
+
After migrating all of your redis keys, update `Redis::Objects.prefix_style = :modern` to
|
|
58
|
+
start using the new keys.
|
|
59
|
+
|
|
60
|
+
**Note: Your existing data in Redis will not be accessible after running `migrate_redis_legacy_keys`
|
|
61
|
+
until the `prefix_style` has been changed.**
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# Overview
|
|
66
|
+
|
|
67
|
+
This is **not** an ORM ([Object-Relational Mapping](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping)).
|
|
68
|
+
People that are wrapping ORM’s around Redis are missing the point.
|
|
9
69
|
|
|
10
70
|
The killer feature of Redis is that it allows you to perform _atomic_ operations
|
|
11
|
-
on _individual_ data structures, like counters, lists, and sets.
|
|
71
|
+
on _individual_ data structures, like counters, lists, and sets. The **atomic** part is HUGE.
|
|
12
72
|
Using an ORM wrapper that retrieves a "record", updates values, then sends those values back,
|
|
13
|
-
_removes_ the atomicity, and thus the major advantage of Redis.
|
|
73
|
+
_removes_ the atomicity, and thus the major advantage of Redis. Just use MySQL, k?
|
|
14
74
|
|
|
15
75
|
This gem provides a Rubyish interface to Redis, by mapping [Redis data types](http://redis.io/commands)
|
|
16
|
-
to Ruby objects, via a thin layer over the `redis` gem.
|
|
76
|
+
to Ruby objects, via a thin layer over the `redis` gem. It offers several advantages
|
|
17
77
|
over the lower-level redis-rb API:
|
|
18
78
|
|
|
19
|
-
1. Easy to integrate directly with existing ORMs - ActiveRecord, DataMapper, etc.
|
|
79
|
+
1. Easy to integrate directly with existing ORMs - ActiveRecord, DataMapper, etc. Add counters to your model!
|
|
20
80
|
2. Complex data structures are automatically Marshaled (if you set :marshal => true)
|
|
21
81
|
3. Integers are returned as integers, rather than '17'
|
|
22
82
|
4. Higher-level types are provided, such as Locks, that wrap multiple calls
|
|
@@ -25,46 +85,50 @@ This gem originally arose out of a need for high-concurrency atomic operations;
|
|
|
25
85
|
for a fun rant on the topic, see [An Atomic Rant](http://nateware.com/2010/02/18/an-atomic-rant),
|
|
26
86
|
or scroll down to [Atomic Counters and Locks](#atomicity) in this README.
|
|
27
87
|
|
|
28
|
-
There are two ways to use Redis::Objects, either as an include
|
|
29
|
-
tightly integrate with ORMs or other classes), or standalone by using classes such
|
|
88
|
+
There are two ways to use Redis::Objects, either as an include within a [model class](#option-1-model-class-include) (to
|
|
89
|
+
tightly integrate with ORMs or other classes), or [standalone](#option-2-standalone-usage) by using classes such
|
|
30
90
|
as `Redis::List` and `Redis::SortedSet`.
|
|
31
91
|
|
|
32
|
-
Installation and Setup
|
|
33
|
-
|
|
92
|
+
# Installation and Setup
|
|
93
|
+
|
|
34
94
|
Add it to your Gemfile as:
|
|
35
95
|
|
|
36
|
-
|
|
96
|
+
```ruby
|
|
37
97
|
gem 'redis-objects'
|
|
38
|
-
|
|
98
|
+
```
|
|
39
99
|
|
|
40
|
-
Redis::Objects needs a handle created by `Redis.new` or a [ConnectionPool](https://github.com/mperham/connection_pool)
|
|
100
|
+
Redis::Objects needs a handle created by `Redis.new` or a [ConnectionPool](https://github.com/mperham/connection_pool).
|
|
101
|
+
|
|
102
|
+
If you're using Rails, `config/initializers/redis.rb` is a good place for this.
|
|
103
|
+
However, there are **no** dependencies on Rails. Redis::Objects can be used in any Ruby code; Sinatra,
|
|
104
|
+
Resque, or Standalone - no problem.
|
|
41
105
|
|
|
42
106
|
The recommended approach is to use a `ConnectionPool` since this guarantees that most timeouts in the `redis` client
|
|
43
|
-
do not pollute your existing connection.
|
|
44
|
-
|
|
45
|
-
|
|
107
|
+
do not pollute your existing connection.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
46
110
|
require 'connection_pool'
|
|
47
111
|
Redis::Objects.redis = ConnectionPool.new(size: 5, timeout: 5) { Redis.new(:host => '127.0.0.1', :port => 6379) }
|
|
48
|
-
|
|
112
|
+
```
|
|
49
113
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Redis.current = Redis.new(:host => '127.0.0.1', :port => 6379)
|
|
53
|
-
~~~
|
|
54
|
-
|
|
55
|
-
(If you're on Rails, `config/initializers/redis.rb` is a good place for this.)
|
|
56
|
-
Remember you can use Redis::Objects in any Ruby code. There are **no** dependencies
|
|
57
|
-
on Rails. Standalone, Sinatra, Resque - no problem.
|
|
114
|
+
However, you need to make sure that both `:timeout` and `:size` are set appropriately
|
|
115
|
+
in a multithreaded environment.
|
|
58
116
|
|
|
59
117
|
Alternatively, you can set the `redis` handle directly:
|
|
60
118
|
|
|
61
|
-
|
|
119
|
+
```ruby
|
|
62
120
|
Redis::Objects.redis = Redis.new(...)
|
|
63
|
-
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Redis::Objects will also default to `Redis.current` if `Redis::Objects.redis` is not set.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Redis.current = Redis.new(:host => '127.0.0.1', :port => 6379)
|
|
127
|
+
```
|
|
64
128
|
|
|
65
129
|
Finally, you can even set different handles for different classes:
|
|
66
130
|
|
|
67
|
-
|
|
131
|
+
```ruby
|
|
68
132
|
class User
|
|
69
133
|
include Redis::Objects
|
|
70
134
|
end
|
|
@@ -75,16 +139,16 @@ end
|
|
|
75
139
|
# you can also use a ConnectionPool here as well
|
|
76
140
|
User.redis = Redis.new(:host => '1.2.3.4')
|
|
77
141
|
Post.redis = Redis.new(:host => '5.6.7.8')
|
|
78
|
-
|
|
142
|
+
```
|
|
79
143
|
|
|
80
144
|
As of `0.7.0`, `redis-objects` now autoloads the appropriate `Redis::Whatever`
|
|
81
|
-
classes on demand.
|
|
145
|
+
classes on demand. Previous strategies of individually requiring `redis/list`
|
|
82
146
|
or `redis/set` are no longer required.
|
|
83
147
|
|
|
84
|
-
Option 1: Model Class Include
|
|
85
|
-
|
|
148
|
+
# Option 1: Model Class Include
|
|
149
|
+
|
|
86
150
|
Including Redis::Objects in a model class makes it trivial to integrate Redis types
|
|
87
|
-
with an existing ActiveRecord, DataMapper, Mongoid, or similar class.
|
|
151
|
+
with an existing ActiveRecord, DataMapper, Mongoid, or similar class. **Redis::Objects
|
|
88
152
|
will work with _any_ class that provides an `id` method that returns a unique value.**
|
|
89
153
|
Redis::Objects automatically creates keys that are unique to each object, in the format:
|
|
90
154
|
|
|
@@ -92,7 +156,7 @@ Redis::Objects automatically creates keys that are unique to each object, in the
|
|
|
92
156
|
|
|
93
157
|
For illustration purposes, consider this stub class:
|
|
94
158
|
|
|
95
|
-
|
|
159
|
+
```ruby
|
|
96
160
|
class User
|
|
97
161
|
include Redis::Objects
|
|
98
162
|
counter :my_posts
|
|
@@ -111,15 +175,15 @@ user.my_posts.reset
|
|
|
111
175
|
puts user.my_posts.value # 0
|
|
112
176
|
user.my_posts.reset 5
|
|
113
177
|
puts user.my_posts.value # 5
|
|
114
|
-
|
|
178
|
+
```
|
|
115
179
|
|
|
116
180
|
Here's an example that integrates several data types with an ActiveRecord model:
|
|
117
181
|
|
|
118
|
-
|
|
182
|
+
```ruby
|
|
119
183
|
class Team < ActiveRecord::Base
|
|
120
184
|
include Redis::Objects
|
|
121
185
|
|
|
122
|
-
|
|
186
|
+
redis_lock :trade_players, :expiration => 15 # sec
|
|
123
187
|
value :at_bat
|
|
124
188
|
counter :hits
|
|
125
189
|
counter :runs
|
|
@@ -129,13 +193,21 @@ class Team < ActiveRecord::Base
|
|
|
129
193
|
list :coaches, :marshal => true
|
|
130
194
|
set :outfielders
|
|
131
195
|
hash_key :pitchers_faced # "hash" is taken by Ruby
|
|
132
|
-
|
|
196
|
+
|
|
197
|
+
# Customized keys
|
|
198
|
+
counter :player_totals, :key => 'players/#{username}/total'
|
|
199
|
+
list :all_player_stats, :key => 'players:all_stats', :global => true
|
|
200
|
+
set :total_wins, :key => 'players:#{id}:all_stats'
|
|
201
|
+
value :my_rank, :key => 'players:my_rank:#{username}'
|
|
202
|
+
|
|
203
|
+
def id; @id; end
|
|
204
|
+
def username; "user#{id}"; end
|
|
133
205
|
end
|
|
134
|
-
|
|
206
|
+
```
|
|
135
207
|
|
|
136
|
-
Familiar Ruby array operations Just Work
|
|
208
|
+
Familiar Ruby array operations Just Work™:
|
|
137
209
|
|
|
138
|
-
|
|
210
|
+
```ruby
|
|
139
211
|
@team = Team.find_by_name('New York Yankees')
|
|
140
212
|
@team.on_base << 'player1'
|
|
141
213
|
@team.on_base << 'player2'
|
|
@@ -146,11 +218,11 @@ Familiar Ruby array operations Just Work (TM):
|
|
|
146
218
|
@team.on_base.length # 1
|
|
147
219
|
@team.on_base.delete('player2')
|
|
148
220
|
@team.on_base = ['player1', 'player2'] # ['player1', 'player2']
|
|
149
|
-
|
|
221
|
+
```
|
|
150
222
|
|
|
151
223
|
Sets work too:
|
|
152
224
|
|
|
153
|
-
|
|
225
|
+
```ruby
|
|
154
226
|
@team.outfielders << 'outfielder1'
|
|
155
227
|
@team.outfielders << 'outfielder2'
|
|
156
228
|
@team.outfielders << 'outfielder1' # dup ignored
|
|
@@ -160,35 +232,35 @@ Sets work too:
|
|
|
160
232
|
end
|
|
161
233
|
player = @team.outfielders.detect{|of| of == 'outfielder2'}
|
|
162
234
|
@team.outfielders = ['outfielder1', 'outfielder3'] # ['outfielder1', 'outfielder3']
|
|
163
|
-
|
|
235
|
+
```
|
|
164
236
|
|
|
165
237
|
Hashes work too:
|
|
166
238
|
|
|
167
|
-
|
|
239
|
+
```ruby
|
|
168
240
|
@team.pitchers_faced['player1'] = 'pitcher2'
|
|
169
241
|
@team.pitchers_faced['player2'] = 'pitcher1'
|
|
170
242
|
@team.pitchers_faced = { 'player1' => 'pitcher2', 'player2' => 'pitcher1' }
|
|
171
|
-
|
|
243
|
+
```
|
|
172
244
|
|
|
173
245
|
And you can do unions and intersections between objects (kinda cool):
|
|
174
246
|
|
|
175
|
-
|
|
247
|
+
```ruby
|
|
176
248
|
@team1.outfielders | @team2.outfielders # outfielders on both teams
|
|
177
249
|
@team1.outfielders & @team2.outfielders # in baseball, should be empty :-)
|
|
178
|
-
|
|
250
|
+
```
|
|
179
251
|
|
|
180
252
|
Counters can be atomically incremented/decremented (but not assigned):
|
|
181
253
|
|
|
182
|
-
|
|
254
|
+
```ruby
|
|
183
255
|
@team.hits.increment # or incr
|
|
184
256
|
@team.hits.decrement # or decr
|
|
185
257
|
@team.hits.incr(3) # add 3
|
|
186
258
|
@team.runs = 4 # exception
|
|
187
|
-
|
|
259
|
+
```
|
|
188
260
|
|
|
189
261
|
Defining a different method as the `id` field is easy
|
|
190
262
|
|
|
191
|
-
|
|
263
|
+
```ruby
|
|
192
264
|
class User
|
|
193
265
|
include Redis::Objects
|
|
194
266
|
redis_id_field :uid
|
|
@@ -197,84 +269,100 @@ end
|
|
|
197
269
|
|
|
198
270
|
user.uid # 195137a1bdea4473
|
|
199
271
|
user.my_posts.increment # 1
|
|
200
|
-
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
You can also define globals redis attributes that are accessed through the class itself.
|
|
275
|
+
No id needed/used for these.
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
class Team < ActiveRecord::Base
|
|
279
|
+
include Redis::Objects
|
|
280
|
+
|
|
281
|
+
sorted_set :rank, :global => true
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
Team.rank['Yankees'] = 12
|
|
285
|
+
Team.rank['Red Socks'] = 5
|
|
286
|
+
Team.rank['Mariners'] = 7
|
|
287
|
+
Team.rank.members(:with_scores => true) # => [["Red Socks", 5], ["Mariners", 7], ["Yankees", 12]]
|
|
288
|
+
```
|
|
201
289
|
|
|
202
290
|
Finally, for free, you get a `redis` method that points directly to a Redis connection:
|
|
203
291
|
|
|
204
|
-
|
|
292
|
+
```ruby
|
|
205
293
|
Team.redis.get('somekey')
|
|
206
294
|
@team = Team.new
|
|
207
295
|
@team.redis.get('somekey')
|
|
208
296
|
@team.redis.smembers('someset')
|
|
209
|
-
|
|
297
|
+
```
|
|
210
298
|
|
|
211
299
|
You can use the `redis` handle to directly call any [Redis API command](http://redis.io/commands).
|
|
212
300
|
|
|
213
|
-
Option 2: Standalone Usage
|
|
214
|
-
|
|
301
|
+
# Option 2: Standalone Usage
|
|
302
|
+
|
|
215
303
|
There is a Ruby class that maps to each Redis type, with methods for each
|
|
216
304
|
[Redis API command](http://redis.io/commands).
|
|
217
305
|
Note that calling `new` does not imply it's actually a "new" value - it just
|
|
218
306
|
creates a mapping between that Ruby object and the corresponding Redis data
|
|
219
307
|
structure, which may already exist on the `redis-server`.
|
|
220
308
|
|
|
221
|
-
Counters
|
|
222
|
-
|
|
309
|
+
## Counters
|
|
310
|
+
|
|
223
311
|
The `counter_name` is the key stored in Redis.
|
|
224
312
|
|
|
225
|
-
|
|
313
|
+
```ruby
|
|
226
314
|
@counter = Redis::Counter.new('counter_name')
|
|
227
315
|
@counter.increment # or incr
|
|
228
316
|
@counter.decrement # or decr
|
|
229
317
|
@counter.increment(3)
|
|
230
318
|
puts @counter.value
|
|
231
|
-
|
|
319
|
+
```
|
|
232
320
|
|
|
233
321
|
This gem provides a clean way to do atomic blocks as well:
|
|
234
322
|
|
|
235
|
-
|
|
323
|
+
```ruby
|
|
236
324
|
@counter.increment do |val|
|
|
237
325
|
raise "Full" if val > MAX_VAL # rewind counter
|
|
238
326
|
end
|
|
239
|
-
|
|
327
|
+
```
|
|
240
328
|
|
|
241
329
|
See the section on [Atomic Counters and Locks](#atomicity) for cool uses of atomic counter blocks.
|
|
242
330
|
|
|
243
|
-
Locks
|
|
244
|
-
|
|
331
|
+
## Locks
|
|
332
|
+
|
|
245
333
|
A convenience class that wraps the pattern of [using setnx to perform locking](http://redis.io/commands/setnx).
|
|
246
334
|
|
|
247
|
-
|
|
335
|
+
```ruby
|
|
248
336
|
@lock = Redis::Lock.new('serialize_stuff', :expiration => 15, :timeout => 0.1)
|
|
249
337
|
@lock.lock do
|
|
250
338
|
# do work
|
|
251
339
|
end
|
|
252
|
-
|
|
340
|
+
```
|
|
253
341
|
|
|
254
342
|
This can be especially useful if you're running batch jobs spread across multiple hosts.
|
|
255
343
|
|
|
256
|
-
Values
|
|
257
|
-
|
|
344
|
+
## Values
|
|
345
|
+
|
|
258
346
|
Simple values are easy as well:
|
|
259
347
|
|
|
260
|
-
|
|
348
|
+
```ruby
|
|
261
349
|
@value = Redis::Value.new('value_name')
|
|
262
350
|
@value.value = 'a'
|
|
263
351
|
@value.delete
|
|
264
|
-
|
|
352
|
+
```
|
|
265
353
|
|
|
266
354
|
Complex data is no problem with :marshal => true:
|
|
267
355
|
|
|
268
|
-
|
|
356
|
+
```ruby
|
|
269
357
|
@account = Account.create!(params[:account])
|
|
270
358
|
@newest = Redis::Value.new('newest_account', :marshal => true)
|
|
271
359
|
@newest.value = @account.attributes
|
|
272
360
|
puts @newest.value['username']
|
|
273
|
-
|
|
361
|
+
```
|
|
274
362
|
|
|
275
363
|
Compress data to save memory usage on Redis with :compress => true:
|
|
276
364
|
|
|
277
|
-
|
|
365
|
+
```ruby
|
|
278
366
|
@account = Account.create!(params[:account])
|
|
279
367
|
@marshaled_value = Redis::Value.new('marshaled', :marshal => true, :compress => true)
|
|
280
368
|
@marshaled_value.value = @account.attributes
|
|
@@ -282,13 +370,13 @@ Compress data to save memory usage on Redis with :compress => true:
|
|
|
282
370
|
@unmarshaled_value = 'Really Long String'
|
|
283
371
|
puts @marshaled_value.value['username']
|
|
284
372
|
puts @unmarshaled_value.value
|
|
285
|
-
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Lists
|
|
286
376
|
|
|
287
|
-
Lists
|
|
288
|
-
-----
|
|
289
377
|
Lists work just like Ruby arrays:
|
|
290
378
|
|
|
291
|
-
|
|
379
|
+
```ruby
|
|
292
380
|
@list = Redis::List.new('list_name')
|
|
293
381
|
@list << 'a'
|
|
294
382
|
@list << 'b'
|
|
@@ -303,35 +391,35 @@ Lists work just like Ruby arrays:
|
|
|
303
391
|
@list.pop
|
|
304
392
|
@list.clear
|
|
305
393
|
# etc
|
|
306
|
-
|
|
394
|
+
```
|
|
307
395
|
|
|
308
396
|
You can bound the size of the list to only hold N elements like so:
|
|
309
397
|
|
|
310
|
-
|
|
398
|
+
```ruby
|
|
311
399
|
# Only holds 10 elements, throws out old ones when you reach :maxlength.
|
|
312
400
|
@list = Redis::List.new('list_name', :maxlength => 10)
|
|
313
|
-
|
|
401
|
+
```
|
|
314
402
|
|
|
315
403
|
Complex data types are serialized with :marshal => true:
|
|
316
404
|
|
|
317
|
-
|
|
405
|
+
```ruby
|
|
318
406
|
@list = Redis::List.new('list_name', :marshal => true)
|
|
319
407
|
@list << {:name => "Nate", :city => "San Diego"}
|
|
320
408
|
@list << {:name => "Peter", :city => "Oceanside"}
|
|
321
409
|
@list.each do |el|
|
|
322
410
|
puts "#{el[:name]} lives in #{el[:city]}"
|
|
323
411
|
end
|
|
324
|
-
|
|
412
|
+
```
|
|
325
413
|
|
|
326
414
|
Note: If you run into issues, with Marshal errors, refer to the fix in [Issue #176](https://github.com/nateware/redis-objects/issues/176).
|
|
327
415
|
|
|
328
|
-
Hashes
|
|
329
|
-
|
|
416
|
+
## Hashes
|
|
417
|
+
|
|
330
418
|
Hashes work like a Ruby [Hash](http://ruby-doc.org/core/classes/Hash.html), with
|
|
331
|
-
a few Redis-specific additions.
|
|
419
|
+
a few Redis-specific additions. (The class name is "HashKey" not just "Hash", due to
|
|
332
420
|
conflicts with the Ruby core Hash class in other gems.)
|
|
333
421
|
|
|
334
|
-
|
|
422
|
+
```ruby
|
|
335
423
|
@hash = Redis::HashKey.new('hash_name')
|
|
336
424
|
@hash['a'] = 1
|
|
337
425
|
@hash['b'] = 2
|
|
@@ -341,26 +429,26 @@ end
|
|
|
341
429
|
@hash['c'] = 3
|
|
342
430
|
puts @hash.all # {"a"=>"1","b"=>"2","c"=>"3"}
|
|
343
431
|
@hash.clear
|
|
344
|
-
|
|
432
|
+
```
|
|
345
433
|
|
|
346
434
|
Redis also adds incrementing and bulk operations:
|
|
347
435
|
|
|
348
|
-
|
|
436
|
+
```ruby
|
|
349
437
|
@hash.incr('c', 6) # 9
|
|
350
438
|
@hash.bulk_set('d' => 5, 'e' => 6)
|
|
351
439
|
@hash.bulk_get('d','e') # "5", "6"
|
|
352
|
-
|
|
440
|
+
```
|
|
353
441
|
|
|
354
|
-
Remember that numbers become strings in Redis.
|
|
442
|
+
Remember that numbers become strings in Redis. Unlike with other Redis data types,
|
|
355
443
|
`redis-objects` can't guess at your data type in this situation, since you may
|
|
356
444
|
actually mean to store "1.5".
|
|
357
445
|
|
|
358
|
-
Sets
|
|
359
|
-
|
|
446
|
+
## Sets
|
|
447
|
+
|
|
360
448
|
Sets work like the Ruby [Set](http://ruby-doc.org/core/classes/Set.html) class.
|
|
361
449
|
They are unordered, but guarantee uniqueness of members.
|
|
362
450
|
|
|
363
|
-
|
|
451
|
+
```ruby
|
|
364
452
|
@set = Redis::Set.new('set_name')
|
|
365
453
|
@set << 'a'
|
|
366
454
|
@set << 'b'
|
|
@@ -373,11 +461,11 @@ They are unordered, but guarantee uniqueness of members.
|
|
|
373
461
|
end
|
|
374
462
|
@set.clear
|
|
375
463
|
# etc
|
|
376
|
-
|
|
464
|
+
```
|
|
377
465
|
|
|
378
466
|
You can perform Redis intersections/unions/diffs easily:
|
|
379
467
|
|
|
380
|
-
|
|
468
|
+
```ruby
|
|
381
469
|
@set1 = Redis::Set.new('set1')
|
|
382
470
|
@set2 = Redis::Set.new('set2')
|
|
383
471
|
@set3 = Redis::Set.new('set3')
|
|
@@ -389,22 +477,22 @@ members = @set1 - @set2 # difference
|
|
|
389
477
|
members = @set1.intersection(@set2, @set3) # multiple
|
|
390
478
|
members = @set1.union(@set2, @set3) # multiple
|
|
391
479
|
members = @set1.difference(@set2, @set3) # multiple
|
|
392
|
-
|
|
480
|
+
```
|
|
393
481
|
|
|
394
482
|
Or store them in Redis:
|
|
395
483
|
|
|
396
|
-
|
|
484
|
+
```ruby
|
|
397
485
|
@set1.interstore('intername', @set2, @set3)
|
|
398
486
|
members = @set1.redis.get('intername')
|
|
399
487
|
@set1.unionstore('unionname', @set2, @set3)
|
|
400
488
|
members = @set1.redis.get('unionname')
|
|
401
489
|
@set1.diffstore('diffname', @set2, @set3)
|
|
402
490
|
members = @set1.redis.get('diffname')
|
|
403
|
-
|
|
491
|
+
```
|
|
404
492
|
|
|
405
493
|
And use complex data types too, with :marshal => true:
|
|
406
494
|
|
|
407
|
-
|
|
495
|
+
```ruby
|
|
408
496
|
@set1 = Redis::Set.new('set1', :marshal => true)
|
|
409
497
|
@set2 = Redis::Set.new('set2', :marshal => true)
|
|
410
498
|
@set1 << {:name => "Nate", :city => "San Diego"}
|
|
@@ -415,14 +503,14 @@ And use complex data types too, with :marshal => true:
|
|
|
415
503
|
@set1 & @set2 # Nate
|
|
416
504
|
@set1 - @set2 # Peter
|
|
417
505
|
@set1 | @set2 # all 3 people
|
|
418
|
-
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Sorted Sets
|
|
419
509
|
|
|
420
|
-
Sorted Sets
|
|
421
|
-
-----------
|
|
422
510
|
Due to their unique properties, Sorted Sets work like a hybrid between
|
|
423
|
-
a Hash and an Array.
|
|
511
|
+
a Hash and an Array. You assign like a Hash, but retrieve like an Array:
|
|
424
512
|
|
|
425
|
-
|
|
513
|
+
```ruby
|
|
426
514
|
@sorted_set = Redis::SortedSet.new('number_of_posts')
|
|
427
515
|
@sorted_set['Nate'] = 15
|
|
428
516
|
@sorted_set['Peter'] = 75
|
|
@@ -454,19 +542,21 @@ a Hash and an Array. You assign like a Hash, but retrieve like an Array:
|
|
|
454
542
|
@sorted_set.increment('Nate')
|
|
455
543
|
@sorted_set.incr('Peter') # shorthand
|
|
456
544
|
@sorted_set.incr('Jeff', 4)
|
|
457
|
-
|
|
545
|
+
```
|
|
458
546
|
|
|
459
547
|
The other Redis Sorted Set commands are supported as well; see [Sorted Sets API](http://redis.io/commands#sorted_set).
|
|
460
548
|
|
|
461
549
|
<a name="atomicity"></a>
|
|
462
550
|
Atomic Counters and Locks
|
|
463
|
-
|
|
464
|
-
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
You are probably not handling atomicity correctly in your app. For a fun rant
|
|
465
555
|
on the topic, see [An Atomic Rant](http://nateware.com/an-atomic-rant.html).
|
|
466
556
|
|
|
467
557
|
Atomic counters are a good way to handle concurrency:
|
|
468
558
|
|
|
469
|
-
|
|
559
|
+
```ruby
|
|
470
560
|
@team = Team.find(1)
|
|
471
561
|
if @team.drafted_players.increment <= @team.max_players
|
|
472
562
|
# do stuff
|
|
@@ -476,101 +566,101 @@ else
|
|
|
476
566
|
# reset counter state
|
|
477
567
|
@team.drafted_players.decrement
|
|
478
568
|
end
|
|
479
|
-
|
|
569
|
+
```
|
|
480
570
|
|
|
481
571
|
An _atomic block_ gives you a cleaner way to do the above. Exceptions or returning nil
|
|
482
572
|
will rewind the counter back to its previous state:
|
|
483
573
|
|
|
484
|
-
|
|
574
|
+
```ruby
|
|
485
575
|
@team.drafted_players.increment do |val|
|
|
486
576
|
raise Team::TeamFullError if val > @team.max_players # rewind
|
|
487
577
|
@team.team_players.create!(:player_id => 221)
|
|
488
578
|
@team.active_players.increment
|
|
489
579
|
end
|
|
490
|
-
|
|
580
|
+
```
|
|
491
581
|
|
|
492
582
|
Here's a similar approach, using an if block (failure rewinds counter):
|
|
493
583
|
|
|
494
|
-
|
|
584
|
+
```ruby
|
|
495
585
|
@team.drafted_players.increment do |val|
|
|
496
586
|
if val <= @team.max_players
|
|
497
587
|
@team.team_players.create!(:player_id => 221)
|
|
498
588
|
@team.active_players.increment
|
|
499
589
|
end
|
|
500
590
|
end
|
|
501
|
-
|
|
591
|
+
```
|
|
502
592
|
|
|
503
593
|
Class methods work too, using the familiar ActiveRecord counter syntax:
|
|
504
594
|
|
|
505
|
-
|
|
595
|
+
```ruby
|
|
506
596
|
Team.increment_counter :drafted_players, team_id
|
|
507
597
|
Team.decrement_counter :drafted_players, team_id, 2
|
|
508
598
|
Team.increment_counter :total_online_players # no ID on global counter
|
|
509
|
-
|
|
599
|
+
```
|
|
510
600
|
|
|
511
|
-
Class-level atomic blocks can also be used.
|
|
601
|
+
Class-level atomic blocks can also be used. This may save a DB fetch, if you have
|
|
512
602
|
a record ID and don't need any other attributes from the DB table:
|
|
513
603
|
|
|
514
|
-
|
|
604
|
+
```ruby
|
|
515
605
|
Team.increment_counter(:drafted_players, team_id) do |val|
|
|
516
606
|
TeamPitcher.create!(:team_id => team_id, :pitcher_id => 181)
|
|
517
607
|
Team.increment_counter(:active_players, team_id)
|
|
518
608
|
end
|
|
519
|
-
|
|
609
|
+
```
|
|
520
610
|
|
|
521
|
-
### Locks
|
|
611
|
+
### Locks
|
|
522
612
|
|
|
523
613
|
Locks work similarly. On completion or exception the lock is released:
|
|
524
614
|
|
|
525
|
-
|
|
615
|
+
```ruby
|
|
526
616
|
class Team < ActiveRecord::Base
|
|
527
|
-
|
|
617
|
+
redis_lock :reorder # declare a lock
|
|
528
618
|
end
|
|
529
619
|
|
|
530
620
|
@team.reorder_lock.lock do
|
|
531
621
|
@team.reorder_all_players
|
|
532
622
|
end
|
|
533
|
-
|
|
623
|
+
```
|
|
534
624
|
|
|
535
625
|
Class-level lock (same concept)
|
|
536
626
|
|
|
537
|
-
|
|
627
|
+
```ruby
|
|
538
628
|
Team.obtain_lock(:reorder, team_id) do
|
|
539
629
|
Team.reorder_all_players(team_id)
|
|
540
630
|
end
|
|
541
|
-
|
|
631
|
+
```
|
|
542
632
|
|
|
543
|
-
Lock expiration.
|
|
544
|
-
the unthinkable happen (server failure).
|
|
545
|
-
this.
|
|
633
|
+
Lock expiration. Sometimes you want to make sure your locks are cleaned up should
|
|
634
|
+
the unthinkable happen (server failure). You can set lock expirations to handle
|
|
635
|
+
this. Expired locks are released by the next process to attempt lock. Just
|
|
546
636
|
make sure you expiration value is sufficiently large compared to your expected
|
|
547
637
|
lock time.
|
|
548
638
|
|
|
549
|
-
|
|
639
|
+
```ruby
|
|
550
640
|
class Team < ActiveRecord::Base
|
|
551
|
-
|
|
641
|
+
redis_lock :reorder, :expiration => 15.minutes
|
|
552
642
|
end
|
|
553
|
-
|
|
643
|
+
```
|
|
554
644
|
|
|
555
|
-
Keep in mind that true locks serialize your entire application at that point.
|
|
645
|
+
Keep in mind that true locks serialize your entire application at that point. As
|
|
556
646
|
such, atomic counters are strongly preferred.
|
|
557
647
|
|
|
558
|
-
### Expiration
|
|
648
|
+
### Expiration
|
|
559
649
|
|
|
560
650
|
Use :expiration and :expireat options to set default expiration.
|
|
561
651
|
|
|
562
|
-
|
|
652
|
+
```ruby
|
|
563
653
|
value :value_with_expiration, :expiration => 1.hour
|
|
564
654
|
value :value_with_expireat, :expireat => lambda { Time.now + 1.hour }
|
|
565
|
-
|
|
655
|
+
```
|
|
566
656
|
|
|
567
657
|
:warning: In the above example, `expiration` is evaluated at class load time.
|
|
568
658
|
In this example, it will be one hour after loading the class, not after one hour
|
|
569
659
|
after setting a value. If you want to expire one hour after setting the value,
|
|
570
660
|
please use `:expireat` with `lambda`.
|
|
571
661
|
|
|
572
|
-
Custom serialization
|
|
573
|
-
|
|
662
|
+
## Custom serialization
|
|
663
|
+
|
|
574
664
|
You can customize how values are serialized by setting `serializer: CustomSerializer`.
|
|
575
665
|
The default is `Marshal` from the standard lib, but it can be anything that responds to `dump` and
|
|
576
666
|
`load`. `JSON` and `YAML` are popular options.
|
|
@@ -578,7 +668,7 @@ The default is `Marshal` from the standard lib, but it can be anything that resp
|
|
|
578
668
|
If you need to pass extra arguments to `dump` or `load`, you can set
|
|
579
669
|
`marshal_dump_args: { foo: 'bar' }` and `marshal_load_args: { foo: 'bar' }` respectively.
|
|
580
670
|
|
|
581
|
-
|
|
671
|
+
```ruby
|
|
582
672
|
class CustomSerializer
|
|
583
673
|
def self.dump(value)
|
|
584
674
|
# custom code for serializing
|
|
@@ -592,9 +682,22 @@ end
|
|
|
592
682
|
@account = Account.create!(params[:account])
|
|
593
683
|
@newest = Redis::Value.new('custom_serializer', marshal: true, serializer: CustomSerializer)
|
|
594
684
|
@newest.value = @account.attributes
|
|
595
|
-
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## Under the Hood
|
|
690
|
+
|
|
691
|
+
Redis keys are prefixed to namespace keys with the same names.
|
|
692
|
+
By default the prefix is generated from the embedded class's name.
|
|
693
|
+
But you can also set a custom prefix using `redis_prefix=`.
|
|
694
|
+
|
|
695
|
+
If needed the redis key for a specific attribute of a class can be obtained by:
|
|
696
|
+
`MyClass.redis_field_key(attr_name, primary_id)`
|
|
697
|
+
or for globals
|
|
698
|
+
`MyClass.redis_field_key(attr_name)`
|
|
699
|
+
|
|
700
|
+
## Author
|
|
596
701
|
|
|
597
|
-
|
|
598
|
-
=======
|
|
599
|
-
Copyright (c) 2009-2019 [Nate Wiger](http://nateware.com). All Rights Reserved.
|
|
702
|
+
Copyright (c) 2009-2026 [Nate Wiger](http://nateware.com). All Rights Reserved.
|
|
600
703
|
Released under the [Artistic License](http://www.opensource.org/licenses/artistic-license-2.0.php).
|