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