Redis is a simple and very fast key-value store, that can be used for all kinds of things. Resque, for example, is a system built on Redis for
processing background jobs or even scheduled jobs. Redis can be used for all kinds of different things, and so it has a very generalized API that doesn't
make any assumptions about how you're going to use it. The Redis API includes simple methods like get and set and expire. And the Ruby gem for Redis
is a thin layer over the standard Redis API.
Caching expensive operations with Redis
But when most people use Redis, they tend to use it for caching values in a web application, like you would use memcached. And if you use Redis for caching, then you might find yourself writing the same sort of code structure over and over:
if (something = redis.get(redis_key = "the cache key")).nil?
redis.set(
redis_key,
(something =
# Some really slow calculation or data access happens here.
)
)
redis.expire redis_key, 300 # seconds = 5 minutes.
end
# Use the "something" value that was calculated (or cached) above.
use(something)
It's great that Redis#get and Redis#set and Redis#expire are all so simple. But if you're going to wrap expensive operations in Redis caching frequently, then what you really need is a Redis#cache method.
Monkey patching to the rescue
With Ruby, you can monkey patch anything, so it's not difficult to add a new convenience method to
the Ruby bindings for Redis. We can just open the Redis class and drop in a new method. You can simply add a file called lib/redis_cache.rb to a Ruby project in order to add a cache method ot the Redis API:
class Redis
def cache(key, expire=nil)
if (value = get(key)).nil?
value = yield(self)
set(key, value)
expire(key, expire) if expire
value
else
value
end
end
end
The new Redis#cache method accepts three things: a key argument, an optional expire
argument, and a block of code. First, it checks Redis for a value at the given key. If one exists, then
it returns that value immediately. If one doesn't exist, then it uses the code block to generate a value.
Then it sets the Redis key to that value. Then it sets the expiration, in seconds, on that key, if there
was an expiration argument provided.
This simple code teaches Redis to speak the language of caching, simplifying your high-level application code. Instead of the code pattern shown in the first code sample, distracting the reader from the problem at hand with caching details, the application code can be all about the values that it wants to calculate, with caching wrapped unobtrusively around the meat of the solution code.
For example, from the simple unit tests for the Redis#cache method:
require File.dirname(__FILE__) + '/../test_helper'
require 'redis'
require 'redis_cache'
class RedisCacheTest < Test::Unit::TestCase
def test_cache_block_in_redis
redis = Redis.new
assert 42, redis.cache('key') { 42 }
assert 42, redis.cache('key') { assert false,
'This should never be executed' }
end
def test_cache_method_passes_redis_argument_to_block
redis = Redis.new
assert Redis, redis.cache('key') {|redis| redis.class }
end
end
If you have a do_something method that takes a long time to complete, then you can cache that
method at the key "key" with redis.cache('key') { do_something }. Simple.
Recalculate every time
You might want to disable caching in development and test modes. You can add support for disabling
caching by adding a second optional argument to the Redis#cache method:
class Redis
def cache(key, expire=nil, recalculate=false)
if (value = get(key)).nil? || recalculate
value = yield(self)
set(key, value)
expire(key, expire) if expire
value
else
value
end
end
end
If recalculate is true, then the code block will be executed every time. So you can make
that value true in development and test modes like this:
value = redis.cache('key', 60,
['test', 'development'].include? Rails.env) do
"This will always happen."
end
Add timeout and default parameters
Generally when you use this kind of caching, you're using it to cache the results of some operation that's really slow. If that operation is really slow because it involves the network, then maybe sometimes it might time out, and you might want to specify a default value to use instead when it times out. It's easy to add support for a timeout using Ruby's Timeout class, which is supported all the way back to Ruby 1.8.6.
At this point, it's definitely time to switch to named parameters, so that the code that calls this method will be more clear and readable.
class Redis
def cache(params)
key = params[:key] || raise(":key parameter is required!")
expire = params[:expire] || nil
recalculate = params[:recalculate] || nil
expire = params[:expire] || nil
timeout = params[:timeout] || 0
default = params[:default] || nil
if (value = get(key)).nil? || recalculate
begin
value = Timeout::timeout(timeout) { yield(self) }
rescue Timeout::Error
value = default
end
set(key, value)
expire(key, expire) if expire
value
else
value
end
end
end
Example usage
Here's an example of using the new Redis#cache method in a Rails app to cache the results of a call to the Twitter API, with a five-sectond timeout. This example will return a default value of nil if the Twitter API times out. But you could also pass a :default => { 'something?' } parameter in other scenarios.
require 'redis'
require 'twitter'
require 'json'
redis = Redis.new
twitter_user = 'ryanalynporter'
tweet = redis.cache(
:key => "twitter-user-timeline-#{twitter_user}",
:expire => 300, # seconds, 5 minutes
:timeout => 5, # seconds
# Recalculate every time when this is true.
:recalculate => ['test', 'development'].include?(Rails.env)
) do
Twitter.user_timeline(twitter_user, :count => 1).first.to_json
end
unless tweet.nil?
tweet = JSON.parse(tweet)


