Ruby
Article

Rails Model Caching with Redis

By Vasu K

redisrails

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:

$ wget http://download.redis.io/releases/redis-2.8.18.tar.gz
$ tar xzf redis-2.8.18.tar.gz
$ cd redis-2.8.18
$ make

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.

# app/controllers/category_controller.rb

class CategoryController < ApplicationController
  include CategoryHelper
  def index
    @categories = Category.all
  end
end

# app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    @categories = Category.all
  end
end

# app/views/category/index.html.haml

%h1
  Category Listing
%ul#categories
  - @categories.each do |cat|
      %li
        %h3
          = cat.name
        %p
          = cat.desc

# config.routes.rb

Rails.application.routes.draw do
  resources :languages
  resources :category
end

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.

screen1

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:

gem 'redis'
gem 'redis-namespace'
gem 'redis-rails'
gem 'redis-rack-cache'

Once these gems are installed, instruct Rails to use Redis as the cache store:

# config/application.rb

#...........
config.cache_store = :redis_store, 'redis://localhost:6379/0/cache', { expires_in: 90.minutes }
#.........

The redis-namespace gem allows us to create nice a wrapper around Redis:

# config/initializers/redis.rb 

$redis = Redis::Namespace.new("site_point", :redis => Redis.new)

All the Redis functionality is now available across the entire app through the ‘$redis’ global. Here’s an example of how to access the values in the redis server (fire up a Rails console):

$redis.set("test_key", "Hello World!")

This command will create a new key called “test_key” in Redis with the value “Hello World”. To fetch this value, just do:

$redis.get("test_key")

Now that we have the basics, let’s start by rewriting our helper methods:

# app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    categories =  $redis.get("categories")
    if categories.nil?
      categories = Category.all.to_json
      $redis.set("categories", categories)
    end
    @categories = JSON.load categories
  end
end

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:

# app/views/category/index.html.haml

%h1
  Category Listing
%ul#categories
  - @categories.each do |cat|
    %li
      %h3
        = cat["name"]
      %p
        = cat["desc"]

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 :).

screen2

Managing Cache

I just noticed a typo in one of the categories. Let’s fix that first:

$ rails c

c = Category.find_by :name => "Famly and Frends"
c.name = "Family and Friends"
c.save

Reload and see if this update shows up in the view:

screen3

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:

# app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    categories =  $redis.get("categories")
    if categories.nil?
      categories = Category.all.to_json
      $redis.set("categories", categories)
      # Expire the cache, every 3 hours
      $redis.expire("categories",3.hour.to_i)
    end
    @categories = JSON.load categories
  end
end

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:

# app/models/category.rb

class Category
  #...........
  after_save :clear_cache

  def clear_cache
    $redis.del "categories"
  end
  #...........
end

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.

Hope enjoyed reading this. Please share your thoughts in the comments.

Comments
juanpastas

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?

Caching always makes me thing in a lot of questions.

rudi_kramer

Hi Vasu,

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.

So your helper would be reduced to something like the following.

@categories = Rails.cache.fetch("categories") do
  Category.all
end

To add a time based expiry you just add in the time as an option.

@categories = Rails.cache.fetch("categories", expires_in: 5.minutes) do
  Category.all
end

You can also clear the specific cache by passing in the key.

Rails.cache.delete("categoriess")

I would defintely recommend checking out http://guides.rubyonrails.org/caching_with_rails.html for more information.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Ruby, once a week, for free.