Replacing CouchDB with Redis Cache for Heavy Rails Queries
This article explains how a failing CouchDB‑based Rails cache for a massive SQL report was redesigned using Redis, detailing configuration, a producer‑consumer pattern, service encapsulation, and scheduled rake tasks to achieve stable, fast page loads.
When a complex report backed by a several‑hundred‑line SQL query caused severe performance problems on a high‑traffic page, the team initially cached the result with Rails.cache using CouchDB as the backend.
# config/environments/production.rb
couch_host = YAML.load_file(Rails.root.join("config/couch.yml")).symbolize_keys[:host]
config.cache_store = :mem_cache_store, couch_host , { :namespace => 'rails_cache' } # config/couch.yml
host: xxx.xxx.xxx.xxx:xxxBecause the project already used Redis and the old Rails version (3.0.9) prevented the Redis gems from working, the developers decided to replace CouchDB with Redis.
# Gemfile
gem 'rails', '3.0.9'
#gem 'redis-rails', '3.1.3'
#gem "redis-store", "~> 1.0.0"The new design follows three steps:
Use Redis instead of CouchDB, leveraging existing familiarity and stability.
Front‑end requests read only from Redis; if the cache is empty they receive an empty array, protecting the database from heavy load.
Set the cache expiration to 999 days (effectively never) and run a daily rake task to refresh the cache.
This pattern forms a classic producer‑consumer model: a rake task (producer) generates the query result each day, while front‑end requests (consumers) read the cached data.
Redis configuration:
# config/redis_store.yml
cache:
host: xxx.xxx.xxx.xxx
port: xxx
db: 2
driver: hiredis
thread_safe: true
timeout: 200
# config/initializers/redis.rb
$redis = Redis.new(YAML.load_file("#{Rails.root}/config/redis_store.yml").symbolize_keys[:cache])A global $redis variable is wrapped in a service to simplify testing and future extensions.
class ReportCacheService
def initialize(data = {})
@key = data[:key]
@expire_in = data[:expire_in]
@refresh_cache = data[:refresh_cache]
end
def call(&block)
res = @refresh_cache == true ? nil : read()
if res.nil?
res = block.call
write(res)
end
res
end
def read
value = $redis.get(@key)
JSON.parse(value) if value.present?
end
def write(res)
value = res.to_json
$redis.set(@key, value)
$redis.expire(@key, @expire_in)
true
end
endThe report model uses this service to warm the cache:
# app/models/stm_report.rb
class StmReport
NON_EXPIRED = 999.days.seconds
def self.warm_cache(params = {})
cs = ReportCacheService.new(
key: build_cache_key(params),
refresh_cache: true,
expire_in: NON_EXPIRED
)
cs.call do
# heavy work
...
end
end
endA daily rake task triggers the warm‑up:
# lib/tasks/cron.rake
task :warn_stm_report_cache => :environment do
puts "#{DateTime.now}: start cron warm cache"
StmReport.warm_cache(qualified: true)
StmReport.warm_cache(qualified: true, per_page: 50)
puts "#{DateTime.now}: end cron warm cache"
endConsumers read the cached data, falling back to an empty array when the cache is missing:
# app/models/stm_report.rb (continued)
def self.create(params = {})
cs = ReportCacheService.new(key: build_cache_key(params))
items = cs.read
items = [] if items.nil?
# other work
...
endAfter redeploying with Redis, the report became stable and fast, confirming the success of the redesign.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
