Introduction to Using Redis with Rails

Redis is a key-value store that stands out from others, like memcached, in that it has built-in support for data structures like lists, sets, and hashes, and that it can persist data to disk. As such, it is quite useful as both a cache store and as a full-fledged, NoSQL data store. In this article, we will walk through a basic usage example in order to learn how to use Redis within a Rails application.

A Basic Example

Imagine that we have written a simple blogging platform in Rails. We use MySQL as our main database, which is where we store all post content, comments, and user accounts. Let’s say it’s hosted at myrailsblog.com. We get regular traffic directly to posts, thanks to our game-changing social media strategy. There are direct links to these posts all over the internet.

The one thing we’re not crazy about is that a URL to a post looks like this: http://myrailsblog.com/321. Aside from the fact that it’s a little ugly and exposes internal ids, we could get better search engine rankings if we implemented slugs for post URLs. We would like the URL to look like this instead: http://myrailsblog.com/using-redis-with-rails.

So we decide that from now on, posts are going to have slugs which are auto-generated from the title. We will find posts by slug rather than by id. When a new post is created, it will generate a slug which gets stored in a new column of our posts table. However, our platform allows editing of post titles, and furthermore, we would like authors to be able to customize the slug in case the auto-generated one turns out to be undesirable for any reason. This means that a post URL can change after it has been published. What happens when someone follows an old URL? Rather than awkwardly spitting back a 404, our platform should find the correct post from the old slug. This means that all old slugs need to continue to reference the post.

Our natural approach as Rails developers to finding a post by its slug would be to implement something like this:

The questions start flying when we think about the “what do we do here?” bit. Conceptually, we need to check the :id parameter against the post’s old slugs to see if any of them match. One possible approach would be to create a Slug model, and set Posts to have_many :slugs.

However, we can solve this problem a little more elegantly with Redis by mapping slugs to post ids. We can utilize the hash data type to store all slugs under one main key. We will get O(1) lookup time, and we won’t need to migrate the database. In the end, finding a post will look like this instead:

Adding Redis

Getting up and running with Redis is as simple as compiling the server, running it, and connecting to it. Downloading and installing Redis is outside the scope of this article, but there are detailed instructions here. Once it has been successfully installed, it can be run by opening a new terminal window and typing redis-server.

Now that the server is running, we need to let our app talk to it. First, we need to install the redis gem. With bundler, this is as simple as adding the following to the Gemfile, and running bundle install:

We then need to connect to the server and store that connection as a global resource. Additionally, we are going to use the “redis-namespace” gem to organize everything under one application-wide namespace. This will help enormously down the road when multiple apps use the same Redis server. Create a file called “redis.rb” in your config/initializers folder with the following:

That’s it. We may now issue Redis commands directly to the $redis global variable. Let’s test it out by opening a Rails console and executing the following commands:

Cool. We can see that the instance methods of the Redis class are the same as the the Redis commands, found here. For simple get/set operations, we can also use array notation:

Now we are ready to dig a little deeper.

The Slug Model

Fetching

The Slug model has one main purpose: to turn a slug into a post id. We are going to utilize the hash data type in order to avoid polluting the key space with a bunch of slugs. As such, all slugs will reside under the single key, “slugs.” Our first pass at app/models/slug.rb looks like this:

We simply fetch the value of a specific slug within the hash “slugs.” If the key doesn’t exist, Redis returns nil, which will cause our PostsController to redirect to the root path. Fetching is easy, but we need to be able to store slug/id mappings for this to have any effect.

Storing

Storing values is simple enough as well. We can add a PostObserver to our app with the following code:

Then, we can modify our Slug model to allow for mapping a slug to a post id:

Deleting

The last thing we’ll need to handle is clearing out unused slugs when a post is destroyed. This is because we will need to validate slug uniqueness against all slugs. If a slug points to a post that no longer exists, it will be unusable for future posts. We will revisit this a bit later.

As it turns out, deleting slugs presents an interesting problem: how do we know which slugs reference a particular post id? As we have seen, we can easily find a post id given a slug, but we don’t have an easy way to find all slugs that reference a given post id. With our current implementation, we would have to search every slug in the hash and select only those which map to a given post id. It would be a linear-time operation over the total number of slugs, and this is way too slow. This problem is also a good excuse to introduce Redis’s “set” data type.

The change is just a bit of simple bookkeeping. Every post id will have an associated set whose members are the slugs which map to that post id. That is, whenever we map a slug to a post id, we will also add that slug to the set of slugs associated with that post id. Moreover, as it is possible to update a slug to point to a different post id, we will first need to remove that slug from the set of slugs for the old post id.
Here’s the change:

Now that we have an efficient way to fetch all slugs for a given post id, we are ready to implement the destroy method:

We will destroy the mapping from the PostObserver:

Uniqueness

As mentioned above, another important aspect of our slug implementation is making sure that a post never has a slug that is mapped to another. In our Post model, we must implement a validation that ensures there is a slug, and that it either does not point to a post id, or it points to the same post we’re currently saving (in the case of an update):

Note that this means that slug auto-generation needs to take place before validation.

Save Post IDs as Slugs

The last little bit of bookkeeping involves mapping post ids to themselves in Redis. As mentioned above, there are links all over the internet in this format: http://myrailsblog.com/321. After we launch our slug implementation, when someone visits a post URL, we will look for “321″ in Redis to see if it maps to a post id. Unless we want all of these old URLs to redirect to the homepage, we’ll need to run a simple script:

Of course, the other option is to modify the PostController to also find Posts by id in the event that Redis has no mapping, but this is slightly less efficient:

All Done

With that, we have successfully implemented slugs for our blogging platform. Even if an old post URL is followed, our platform will still find the correct post. Moreover, we did all of this without needing to modify the database schema, and we get the benefits of the speedy lookup time of Redis.

I hope you have found this example useful, and continue to find creative ways to use Redis on your own.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Vlad

    Thanks for this post. A small thing that confused me is that you call Slug.delete on after_destroy, not Slug.destroy. I can’t understand how Slug class maps those methods.

    • Peter Brindisi

      You are correct. It should be “destroy” in the after_destroy. I noticed the error too, and I’ve updated the gist, but it hasn’t been pulled into the article yet. If you view the gist on github, you’ll see the corrected code.