You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Rails stale? helper时出现ETag延迟及缓存异常问题

解决Rails菜单缓存更新后一分钟内频繁重建的问题

看起来你遇到的核心问题是:用户对象更新后的一分钟内,服务器每次都会重新生成菜单(调用Menu.new()),而不是返回304缓存响应,之后才恢复正常。同时缓存响应返回200而非预期的304,这说明stale?方法一直在判定资源"过期"。下面我们一步步分析原因并给出解决方案:

首先排查:用户对象的updated_at是否在频繁变化?

最可能的原因是用户的updated_at字段在更新后的一分钟内被多次修改,导致stale?每次都认为资源已过期。比如很多应用会跟踪用户的last_seen_at,如果这个字段的更新触发了updated_at的变化(默认情况下会),那每次请求都会让用户对象的updated_at刷新,进而让stale?返回true,强制重新生成菜单。

验证方法:添加日志排查

在你的index方法里加入日志,看看每次请求时用户的updated_at是否一致:

def index 
  latest_user = User.find(session.user_id) 
  # 打印关键信息到日志
  Rails.logger.info "=== Menu Cache Check ==="
  Rails.logger.info "User ID: #{latest_user.id}, Updated At: #{latest_user.updated_at.iso8601(6)}"
  Rails.logger.info "Request If-Modified-Since: #{request.headers['If-Modified-Since']}"
  Rails.logger.info "Request If-None-Match: #{request.headers['If-None-Match']}"

  if stale?(etag: latest_user, last_modified: latest_user.updated_at) 
    Rails.logger.info "⚠️  Generating new menu (resource is stale)"
    @menu_info = Menu.new(latest_user) 
    render partial: 'navigation/menus/slide_out', layout: false 
  else
    Rails.logger.info "✅  Returning 304 (cache hit)"
  end
end

查看Rails日志,如果每次请求的Updated At都在变化,那就是这个问题了。

解决方案:避免无关字段更新触发updated_at

如果是last_seen_at这类非核心字段导致的updated_at变化,用update_column代替update/save,它不会触发updated_at更新:

# 在更新last_seen_at的地方替换成:
user.update_column(:last_seen_at, Time.current)

或者在User模型里禁止特定字段的touch操作:

class User < ApplicationRecord
  def touch(name = nil)
    # 当更新last_seen_at时,不更新updated_at
    return super unless name == :last_seen_at
  end
end

其次:确保stale?的时间/ETag精度足够

虽然你的请求头里If-Modified-Since包含秒级时间,但有时候Rails处理时间比较时可能存在时区或精度问题。可以尝试以下优化:

1. 仅依赖ETag(更精确)

ETag基于用户对象的cache_key,包含updated_at的精确值,比last_modified更可靠。简化stale?的调用:

if stale?(latest_user)
  # 生成菜单逻辑
end

Rails会自动用用户的cache_key作为ETag,updated_at作为last_modified。

2. 手动生成高精度ETag

如果担心updated_at的精度被截断,手动生成包含微秒级时间的ETag:

user_identifier = "#{latest_user.id}-#{latest_user.updated_at.iso8601(6)}"
if stale?(etag: user_identifier, last_modified: latest_user.updated_at)
  # 生成菜单逻辑
end

这样ETag会精确到微秒,避免因时间精度丢失导致的误判。

检查:Menu.new()是否会修改用户对象?

如果Menu.new(latest_user)的初始化逻辑里有修改用户字段的操作(比如touch、更新某个属性),那每次生成菜单都会刷新用户的updated_at,导致循环触发stale?返回true。

检查Menu类的代码,确保它只读取用户数据,不做任何修改操作:

class Menu
  def initialize(user)
    @user = user
    # 只做读取操作,比如@permissions = user.permissions,不要修改user
  end
end

关于304状态码不返回的问题

Rails的stale?方法在判定资源未过期时,会自动返回304响应,不需要手动处理。如果一直返回200,说明stale?每次都返回true,核心原因还是用户对象的updated_at在频繁变化,或者ETag/last_modified的判定逻辑有问题。按照前面的步骤排查后,这个问题应该会同步解决。

最后验证缓存行为

修复后,再次测试:

  1. 用户首次访问:生成菜单,返回200,携带新的ETag和Last-Modified。
  2. 后续访问:服务器返回304,不生成菜单。
  3. 更新用户对象:服务器返回200新菜单,之后再次访问返回304。

这样就符合你的预期了。

内容的提问来源于stack exchange,提问作者Mark Kadlec

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 06:48:47