Rails Model Caching with Redis

Model level caching is something that’s often ignored, even by seasoned developers. Much of it’s due to the misconception that, when you cache the views, you don’t need to cache at the lower levels. While it’s true that much of a bottleneck in the Rails world lies in the View layer, that’s not always the case.

Lower level caches are very flexible and can work anywhere in the application. In this tutorial, I’ll demonstrate how to cache your models with Redis.

How Caching Works?

Traditionally, accessing disk has been expensive. Trying to access data from the disk frequently will have an adverse impact on performance. To counter this, we can implement a caching layer in between your application and the database server.

A caching layer doesn’t hold any data at first. When it receives a request for the data, it calls the database and stores the result in memory (the cache). All subsequent requests will be served from the cache layer, so the unnecessary roundtrip to the database server is avoided, improving performance.

Why Redis?

Redis is an in-memory, key-value store. It’s blazingly fast and data retrieval is almost instantaneous. Redis supports advanced data structures like lists, hashes, sets, and can persist to disk.

While most developers prefer Memcache with Dalli for their caching needs, I find Redis very simple to setup and easy to administer. Also, if you are using resque or Sidekiq for managing your background jobs, you probably have Redis installed already. For those who are interested in knowing when to use Redis, this discussion is a good place to start.

Prerequisites

I’m assuming you have Rails up and running. The example here uses Rails 4.2.rc1, haml to render the views, and MongoDB as the database, but the snippets in this tutorial should be compatible with any version of Rails.

You also need to have Redis installed and running before we get started. Move into your app directory, and execute the following commands:

The command is going to take a while to complete. Once it has completed, just start the Redis server:

$ cd redis-2.8.18/src
$ ./redis-server

To measure the performance improvement, we will use the gem “rack-mini-profiler”. This gem will help us measure the performance improvement right from the views.

Getting Started

For this example, let’s build a fictional online story reading store. This store has books in various Categories and Languages. Let’s create the models first:

# app/models/category.rb
class Category
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Paranoia
include CommonMeta
end
# app/models/language.rb
class Language
include Mongoid: :Document
include Mongoid::Timestamps
include Mongoid::Paranoia
include CommonMeta
end
# app/models/concerns/common_meta.rb
module CommonMeta
extend ActiveSupport::Concern
included do
field :name, :type => String
field :desc, :type => String
field :page_title, :type => String
end
end

I’ve included a seed file here. Just copy paste this into your seeds.rb and run the rake seed task to dump data into our database.

rake db:seed

Now, let us create a simple category listing page that shows all the categories available with a description and tags.

When you fire up your browser and point it to /category, you’ll find mini-profiler benchmarking the execution time of each action performed in the backend. This should give you a fair idea which parts of your application are slow and how to optimize them. This page has executed two SQL commands and the query had taken around 5ms to complete.

While 5ms may seem trivial at first, especially with the views taking more time to render, in a production-grade application there are typically several database queries, so they can slow down the application considerably.

Since the metadata models are unlikely to change that often it makes sense to avoid unnecessary database roundtrips. This is where lower level caching comes in.

Initialize Redis

There is a Ruby client for Redis that helps us to connect to the redis instance easily:

The first time this code executes there won’t be anything in memory/cache. So, we ask Rails to fetch it from the database and then push it to redis. Notice the to_json call? When writing objects to Redis, we have a couple of options. One option is to iterate over each property in the object and then save them as a hash, but this is slow. The simplest way is to save them as a JSON encoded string. To decode, simply use JSON.load.

However, this comes in with an unintended side effect. When we’re retrieving the values, a simple object notation won’t work. We need to update the views to use the hash syntax to display the categories:

Fire up the browser again and see if there is a performance difference. The first time, we still hit the database, but on subsequent reloads, the database is not used at all. All future requests will be loaded from the cache. That’s a lot of savings for a simple change :).

Nope, this change is not reflected in our views. This is because we’ve bypassed accessing the database and all values are served from the cache. Alas, the cache is now stale and the updated data won’t be available until Redis is restarted. This is a deal breaker for any application. We can overcome this issue by expiring the cache periodically:

This will expire the cache every 3 hours. While this works for most scenarios, the data in the cache will now lag the database. This likely will not work for you. If you prefer to keep the cache fresh, we can use an after_save callback:

Every time the model is updated, we’re instructing Rails to clear the cache. This will ensure that the cache is always up to date. Yay!

You should probably be using something like cache_observers in production, but for brevity sake we will stick with after_save here. In case you’re not sure which approach might work best for you, this discusssion might shed some light.

Conclusion

Lower level caching is very simple and, when properly used, it is very rewarding. It can instantaneously boost your system’s performance with minimal effort. All the code snippets in this article are available on Github.

Hola! I'm a Fullstack developer and a strong advocate of Mobile first design. I'm running a digital children's startup for kids and I lead the engineering efforts there. In my free time I ramble about technology, and consult startups.

Replies

Thanks for the article, caching requires a lot of effort, it seems you have to figure out a lot of things without any framework, just using creativity, to know what to cache, how to cache, what and when to invalidate.

In your article, if application is too big you could use a Struct to not to have to change views, is that possible? or does ruby(array of objects) -> cache(string), then cache(string) -> ruby(array of objects) consumes more resources than cache(string) -> ruby(array of hashes)?

Would you cache just one model? or maybe expire just the updated model? why? what if the database has a lot of records? you might require a paginated cache, right?

I think you are definitely on the right track but you have missed a trick which will make your code much cleaner, namely Rails.cache.

This rails class ties in to the config.cache_store that you set earlier and allows you to access the cache by using Rails.cache in your code so you no longer need to set up the $redis initializer and any time you want to swap out your cache backend you can easily do so.

This also means that you know have access to Rails.cache.fetch which takes a key and then any optional options and then a value or a block. In the case of a block, calling Rails.cache.fetch will first check the cache using the key provided and it doesn't find anything in the cache then it executes the do block and sets the cache to whatever the block yields.