Redis cache in Rails


Let’s imagine, what we have heavy request in our application, for example for 10k jobs.

You can just populate database with fake data:

10000.times do
  Job.create(title: Faker::Job.title, email: Faker::Internet.email, description: Faker::Lorem.paragraph(2), company: Faker::Company.name)
end

Let’s start cache implementation, based on Redis.

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

And our /config/initializers/redis.rb:

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

Let’s check our jobs/index page without caching implementation.

About 5000ms - not good.

# 16ms to SQL SELECT
Started GET "/" for ::1 at 2018-02-04 22:04:41 +0300
Processing by JobsController#index as HTML
  Job Load (15.9ms)  SELECT "jobs".* FROM "jobs"
   app/loaders/cache_loader.rb:9
  Rendering jobs/index.html.erb within layouts/application
  Rendered jobs/index.html.erb within layouts/application (2529.5ms)
Completed 200 OK in 4902ms (Views: 2623.8ms | ActiveRecord: 15.9ms)

Cache1

Let’s start our cache service realization.

Just what we do here, load all Job data to json (or you can use yaml for example), and store in Redis with special key:

class CacheLoader
  def self.load
    jobs = $redis.get('jobs')
    if jobs.nil?
      jobs = Job.all.to_json
      $redis.set('jobs', jobs)
      $redis.expire('jobs', 10.hours.to_i)
    end
    JSON.load(jobs)
  end
end

Let’s improve our controller action, and make small changes in our view, because we need to iterate via json entries.

@jobs = CacheLoader.load

Let’s check our view again:

# No SQL requests
Started GET "/" for ::1 at 2018-02-04 22:03:53 +0300
Processing by JobsController#index as HTML
  Rendering jobs/index.html.erb within layouts/application
  Rendered jobs/index.html.erb within layouts/application (2667.9ms)
Completed 200 OK in 2961ms (Views: 2700.8ms | ActiveRecord: 0.0ms)

Cache1

Wow! 2700 ms! Much better, but I think we can improve our loader.

Let’s use ActiveRecord pluck method and make some changes in our view.

jobs = Job.pluck(:id, :title, :email, :company).to_json

Cache1

954 ms! Sounds great.

Instead of using Redis, you can use for example Memcached store.

But in upcoming Rails 5.2, Redis cache store will be out-of-the-box feature: https://github.com/rails/rails/pull/31134.

This new Redis Cache Store supports Redis::Distributed, for Memcached-like sharding across Redises. It’s fault tolerant, so will treat failures like misses, rather than kill the request with an exception. It even supports distributed MGETs for that full partial collection caching goodness.