Hi there ,
I’m excited to share a tool we built at CZI to cache database queries and 3rd API calls efficiently and robustly: https://github.com/chanzuckerberg/redis-memo
With a few line of configurations and a bit of magic ,
class Post < ApplicationRecord
extend RedisMemo::MemoizeQuery
memoize_table_column :id
end
all the ActiveRecord querying methods that send a SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1
query will be cached (and invalidated) automatically!!
Post.where(id: [1,2,3]).to_a
Post.find(1)
Post.find_by_id(2)
With RedisMemo, add caching is super easy – you don’t need to change the call sites one by one and worry about breaking things!
It shines when caching records association. With the old fashioned way, you’d need to do something like this:
class User < ApplicationRecord
def invalidate_cache
posts.each do |post| # this could take many seconds
CachedPost.new(post.id).invalidate
end
end
end
If the post
cache contains some information related to a specific user. And of course, looping through all the posts is slow and expensive!
With RedisMemo, invalidation is a super-fast (<= 2ms). If a user record has changed, millions or even billions of accoicated cache entry will be invalidated almost instantly.
class Post < ApplicationRecord
extend RedisMemo::MemoizeMethod
def post_cache
"#{author.display_name} wrote #{title}"
end
memoize_method :post_cache do |post|
depends_on Post.where(id: post.id) # extracts dependency from the ActiveRecord::Relation
depends_on User.where(id: post.author_id) # no database queries being sent
end
end
Prior to RedisMemo, adding caching correctly has been a struggle for us:
- when to invalidate the cache? especially in database transactions
- how to combine the caching API with the ActiveRecord querying interface?
With RedisMemo, my team was able to move half of the database read traffic to Reids, by adding few lines of configurations on some heavily used models
Check it out! You might find it useful