Rails使用cache.fetch实现套餐列表缓存的合理性及优化方案咨询
现有实现合理性判定
当前实现不合理,核心问题是没有区分全局通用的公共套餐数据和项目专属的订阅状态数据的特性,把两类更新频率、适用范围完全不同的数据绑定缓存,会引发多个潜在问题。
存在的问题
- 缓存空间严重浪费:公共套餐是全平台所有项目通用的,当前方案每个项目都单独缓存一份完整的套餐列表,项目量级大时会有大量重复的公共数据占用缓存资源,缓存利用率极低。
- 缓存一致性极难维护:一旦全局套餐数据(比如价格、功能限制、上下架状态)变更,需要遍历所有项目的缓存key逐个淘汰,实现成本极高,漏删缓存就会导致不同项目看到的套餐数据不一致,旧数据长期生效。
- 过期状态逻辑错误:
is_expired字段和日期强相关,哪怕套餐本身、项目订阅都没有变更,只要跨天这个字段的取值就会变化。如果缓存设置的expires_in大于1天,就会出现状态错误,比如昨天还未过期的套餐,今天缓存没更新依然显示未过期。 - 重复查询浪费数据库资源:每次缓存失效时都要重新查询全量公共套餐表,本质是重复查询完全相同的公共数据。
最优缓存方案
核心思路是拆分两级缓存,把全局公共数据和项目专属数据分开缓存,按需在内存中合并返回,内存合并的成本几乎可以忽略不计。
一级缓存:缓存全局公共套餐数据
缓存全平台激活状态的套餐基础属性,只有套餐数据更新时才需要淘汰这个缓存:
# 全局公共套餐缓存,仅当套餐增删改、上下架时失效 global_plans = Rails.cache.fetch("active_plans", expires_in: 7.days) do Plan.where(is_active: true).map(&:attributes) end
二级缓存:缓存项目专属订阅状态
每个项目单独缓存自己的订阅信息、过期状态,仅当前项目订阅变更或者跨天时失效:
# 项目专属状态缓存,仅当项目订阅变更、跨天时失效 project_plan_status = Rails.cache.fetch("project_plan_status/#{project.id}", expires_in: 1.day) do project_plan = project.project_plan next { subscribed_plan_id: nil, is_expired: false } unless project_plan { subscribed_plan_id: project_plan.plan_id, is_expired: project_plan.end_date.to_date <= Date.current } end
内存合并数据返回
不需要缓存合并后的结果,直接在内存中拼接公共数据和专属状态即可:
plan_list = global_plans.map do |plan| subscribed = plan["id"] == project_plan_status[:subscribed_plan_id] expired = subscribed ? project_plan_status[:is_expired] : false plan.merge(is_expired: expired, subscribed: subscribed) end
配套缓存淘汰机制
- 全局套餐数据新增、修改、上下架时,主动删除
active_plans缓存 - 项目订阅变更(新购、续费、更换套餐)时,主动删除对应项目的
project_plan_status/#{project.id}缓存 - 项目状态缓存的过期时间设置为1天,自动处理跨天导致的
is_expired字段变更问题
方案优势
- 缓存利用率极高:全局公共套餐仅存1份,每个项目仅存自己的少量状态数据,缓存空间占用比原方案降低几个量级
- 缓存一致性易维护:全局套餐变更只需要删1个缓存key,项目订阅变更只需要删对应项目的1个key,实现成本极低,不会出现一致性问题
- 性能更高:缓存失效后查询成本极低,最多只需要查一次公共套餐表+一次项目订阅表,大部分请求都可以直接命中两级缓存,内存合并几乎无损耗
内容的提问来源于stack exchange,提问作者Animesh
相关产品推荐
相关产品推荐

