Rails底层缓存:当ActiveRecord对象updated_at变更或集合新增对象时更新缓存
我来给你拆解一下怎么实现这个需求——让底层缓存能感知ActiveRecord对象的updated_at变更,还有集合新增对象时自动更新。其实核心思路和片段缓存的基于键的过期策略是通的:把所有会影响缓存内容的变化因子都融入缓存键,这样一旦因子变化,缓存键就会更新,旧缓存自然失效,Rails会自动生成新的缓存内容。
1. 单个对象的缓存更新:绑定updated_at
当单个ActiveRecord对象的updated_at变更时,我们只需要把对象本身纳入缓存键即可——Rails的ActiveRecord对象自带cache_key方法,这个方法会自动把对象的id和updated_at拼接成唯一键,完美适配基于时间戳的过期需求。
举个实际例子,假设Product模型有一个耗时的计算方法discounted_price,我们用底层缓存来优化:
class Product < ApplicationRecord def discounted_price Rails.cache.fetch([self, "discounted_price"]) do # 这里是你的耗时计算逻辑,比如从第三方接口拉取折扣率再计算 price * fetch_discount_rate end end end
只要Product的updated_at一改变,[self, "discounted_price"]生成的缓存键就会变化,下次调用discounted_price时就会重新计算并缓存新值,完全不用手动删除旧缓存。
如果你想更手动控制键的生成,也可以直接拼接updated_at:
Rails.cache.fetch(["product_#{id}_discounted_price", updated_at.to_i]) do price * fetch_discount_rate end
2. 集合的缓存更新:感知新增/变更
对于集合(比如所有在售商品列表),我们需要覆盖两种变化:集合内对象的更新、新增/删除对象。这时候可以利用Rails集合的cache_key特性——它会自动包含集合的记录数量和所有记录中最大的updated_at,只要其中一个变化,缓存键就会更新。
比如缓存所有活跃商品的名称和价格列表:
class Product < ApplicationRecord def self.active_products_summary active_products = where(status: :active) Rails.cache.fetch([active_products, "active_products_summary"]) do active_products.pluck(:name, :price) end end end
这样一来:
- 当新增一个活跃商品时,集合的
size变了,active_products的cache_key更新; - 当某个活跃商品的
updated_at变更时,集合的最大updated_at变了,cache_key也会更新; - 删除商品同理,集合
size变化触发键更新。
如果你偏好手动删除缓存的方式(比如某些复杂场景下),也可以用回调触发删除:
class Product < ApplicationRecord def self.active_products_summary Rails.cache.fetch("active_products_summary") do where(status: :active).pluck(:name, :price) end end # 当商品创建、更新、删除时,清除集合缓存 after_commit :clear_active_products_cache, on: [:create, :update, :destroy] private def clear_active_products_cache Rails.cache.delete("active_products_summary") end end
不过这种方式需要维护回调,不如利用集合cache_key的方式省心。
3. 额外优化:加版本号应对逻辑变更
如果你的缓存计算逻辑或者返回的数据格式变了(比如把折扣从8折改成7折),可以给缓存键加一个版本号,这样不需要等对象变更就能让所有旧缓存失效:
def discounted_price Rails.cache.fetch([self, "discounted_price", "v2"]) do # 新的计算逻辑 price * 0.7 end end
只要把版本号从v1改成v2,所有相关的旧缓存都会被自动抛弃,直接生成新缓存。
内容的提问来源于stack exchange,提问作者Daniel Viglione

