August 6, 2019
Recyclable cache keys orcacheversioning was introduced in Rails 5.2. Large applications frequently need toinvalidate their cache because cache store has limited memory. We can optimizecache storage and minimize cache miss using recyclable cache keys.
Recyclable cache keys is supported by allcache storesthat ship with Rails.
Before Rails 5.2,cache_key
's format was{model_name}/{id}-{update_at}. Heremodel_name
andid
are always constant for an object andupdated_at
changeson every update.
>> post = Post.last>> post.cache_key=> "posts/1-20190522104553296111"# Update post>> post.touch>> post.cache_key=> "posts/1-20190525102103422069" # cache_key changed
In Rails 5.2,#cache_key
returns{model_name}/{id} and new method#cache_version
returns{updated_at}.
>> ActiveRecord::Base.cache_versioning = true>> post = Post.last>> post.cache_key=> "posts/1">> post.cache_version=> "20190522070715422750">> post.cache_key_with_version=> "posts/1-20190522070715422750"
Let's updatepost
instance and checkcache_key
andcache_version
'sbehaviour.
>> post.touch>> post.cache_key=> "posts/1" # cache_key remains same>> post.cache_version=> "20190527062249879829" # cache_version changed
To use cache versioning feature, we have to enableActiveRecord::Base.cache_versioning
configuration. By defaultcache_versioning
config is set to false for backward compatibility.
We can enable cache versioning configuration globally as shown below.
ActiveRecord::Base.cache_versioning = true# orconfig.active_record.cache_versioning = true
Cache versioning config can be applied at model level.
class Post < ActiveRecord::Base self.cache_versioning = trueend# Or, when setting `#cache_versioning` outside the model -Post.cache_versioning = true
Let's understand the problem step by step with cache keys before Rails 5.2.
1. Writepost
instance to cache usingfetch
api.
>> before_update_cache_key = post.cache_key=> "posts/1-20190527062249879829">> Rails.cache.fetch(before_update_cache_key) { post }=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 06:22:49">
2. Updatepost
instance usingtouch
.
>> post.touch (0.1ms) begin transaction Post Update (1.6ms) UPDATE "posts" SET "updated_at" = ? WHERE "posts"."id" = ? [["updated_at", "2019-05-27 08:01:52.975653"], ["id", 1]] (1.2ms) commit transaction=> true
3. Verify stalecache_key
in cache store.
>> Rails.cache.fetch(before_update_cache_key)=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 06:22:49">
4. Write updatedpost
instance to cache using newcache_key
.
>> after_update_cache_key = post.cache_key=> "posts/1-20190527080152975653">> Rails.cache.fetch(after_update_cache_key) { post }=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">
5. Cache store now has two copies ofpost
instance.
>> Rails.cache.fetch(before_update_cache_key)=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 06:22:49">>> Rails.cache.fetch(after_update_cache_key)=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">
cache_key and its associated instance becomes irrelevant as soon as aninstance is updated. But it stays in cache store until it is manuallyinvalidated.
This sometimes result in overflowing cache store with stale keys and data. Inapplications that extensively use cache store, a huge chunk of cache store getsfilled with stale data frequently.
Now let's take a look at the same example. This time withcache versioning tounderstand how recyclable cache keys help optimize cache storage.
1. Writepost
instance to cache store withversion
option.
>> ActiveRecord::Base.cache_versioning = true>> post = Post.last>> cache_key = post.cache_key=> "posts/1">> before_update_cache_version = post.cache_version=> "20190527080152975653">> Rails.cache.fetch(cache_key, version: before_update_cache_version) { post }=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">
2. Updatepost
instance.
>> post.touch (0.1ms) begin transaction Post Update (0.4ms) UPDATE "posts" SET "updated_at" = ? WHERE "posts"."id" = ? [["updated_at", "2019-05-27 09:09:15.651029"], ["id", 1]] (0.7ms) commit transaction=> true
3. Verify stalecache_version
in cache store.
>> Rails.cache.fetch(cache_key, version: before_update_cache_version)=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">
4. Write updatedpost
instance to cache.
>> after_update_cache_version = post.cache_version=> "20190527090915651029">> Rails.cache.fetch(cache_key, version: after_update_cache_version) { post }=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 09:09:15">
5. Cache store has replaced old copy ofpost
with new version automatically.
>> Rails.cache.fetch(cache_key, version: before_update_cache_version)=> nil>> Rails.cache.fetch(cache_key, version: after_update_cache_version)=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 09:09:15">
Above example shows how recyclable cache keys maintains single, latest copy ofan instance. Stale versions are removed automatically when new version is addedto cache store.
Rails 6 added#cache_versioning
forActiveRecord::Relation
.
ActiveRecord::Base.collection_cache_versioning
configuration should be enabledto use cache versioning feature on collections. It is set to false by default.
We can enable this configuration as shown below.
ActiveRecord::Base.collection_cache_versioning = true# orconfig.active_record.collection_cache_versioning = true
Before Rails 6,ActiveRecord::Relation
hadcache_key
in format{table_name}/query-{query-hash}-{count}-{max(updated_at)}
.
In Rails 6, cache_key is split in stable partcache_key
-{table_name}/query-{query-hash}
and volatile partcache_version
-{count}-{max(updated_at)}
.
For more information, check outblog on ActiveRecord::Relation#cache_key in Rails 5.
>> posts = Post.all>> posts.cache_key=> "posts/query-00644b6a00f2ed4b925407d06501c8fb-3-20190522172326885804"
>> ActiveRecord::Base.collection_cache_versioning = true>> posts = Post.all>> posts.cache_key=> "posts/query-00644b6a00f2ed4b925407d06501c8fb">> posts.cache_version=> "3-20190522172326885804"
Cache versioning works similarly forActiveRecord::Relation
asActiveRecord::Base
.
In case ofActiveRecord::Relation
, if number of records change and/orrecord(s) are updated, then samecache_key
is written to cache store with newcache_version
and updated records.
Previously, cache invalidation had to be done manually either by deleting cacheor setting cache expire duration. Cache versioning invalidates stale dataautomatically and keeps latest copy of data, saving on storage and performancedrastically.
Check out thepull request andcommitfor more details.
If this blog was helpful, check out our full blog archive.