Rails Rails 3 和 Rails 4 中 ETags 工作原理

huopo125 · 2015年04月04日 · 最后由 hww 回复于 2016年08月25日 · 8825 次阅读
本帖已被管理员设置为精华贴

最近和朋友交流的时候,发现有人误以为 ETags 机制是直接通过服务器内存中已保存的信息进行匹配,不需要 Rails 再次生成。其实该结论在 ruby-china 一篇经典帖子 (评论更经典) 中已有结论:总结 web 应用中常用的各种 cache

首次翻译发布,欢迎大家拍砖:)

Etags 是一种 Web 缓存验证机制,并且允许客户端进行缓存协商,能够更加高效的利用客户端的缓存。

Rails 3 和 Rails 4 默认使用的 Etags 机制工作原理

Rails 3 和Rails 4默认使用的Etags机制工作原理 如图,假设我们即将访问一个博客,并请求该博客的列表页面:

  • 首个请求:
    • 浏览器初始化首个请求
  • 首个响应:
    • Rails 生成响应内容
    • Rails 生成 ETag
    • Rails 响应请求,响应信息中带有 ETag 头和状态码 200 浏览器在接收到响应页面后,将缓存该页面。当浏览器在处理后续的请求的时,步骤如下:
  • 后续请求
    • 浏览器发送头部信息带有'If-None-Matched'(存储的是 Etag 值) 的请求
  • 后续响应
    • Rails 生成响应内容和 ETag
    • 比较生成的 ETag 和请求中'If-None-Matched'字段的值
    • 若 ETag 相同,生成的响应内容将不返回浏览器,而将返回 304 状态码
    • 若 ETag 不相同,将返回新的 ETag 值和生成的响应内容

Etag 的优势

那么,使用 ETag 机制来匹配服务器上的内容有什么好处呢?好处就是 Rails 将不发送生成的页面内容,这样响应体将变的更小从而使得其网络中的传输速度更快。浏览器通过加载自身缓存中的内容,使得网站刷新更快,体验更好。

ETag 的使用

在 Rails 中,已经默认使用 ETag 机制,不需要额外操作,以下代码将自动使用 Rails 的默认 ETag 缓存机制

class PostsController < ApplicationController
def show
    @post = Post. find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @post }
    end
  end

  def edit
    @post = Post.find(params[:id])
  end

end

那么 Rails 是如何生成 ETag 信息的呢? 首先,Rails 生成响应内容,并根据生成的响应内容生成 MD5 散列的 ETag,代码类似:

headers['ETag'] = Digest::MD5.hexdigest(body)

ETags 自定义的 ETag

通过每次生成的响应内容来生成 ETag 并不能高效的利用服务器,因为这样服务器将耗时调用数据库和渲染模板文件。 如何避免呢? 方法就是通过 Rails 的 helper 方法 fresh_when 和 stale?来实现。 示例代码:

class PostsController < ApplicationController
  def show
    @post = Post. find(params[:id])
    if stale? @post
      respond_to do |format|
        format.html # show.html.erb
        format.json { render json: @post }
      end
    end
  end

  def edit
    @post = Post.find(params[:id])
    fresh_when @post
  end

end

那么代码中 rails 是如何生成 ETags 呢? Rails 中 helper 方法 stale?和 fresh_when 实现原理如下:

headers['ETag'] = Digest::MD5.hexdigest(@post.cache_key)

cache_key是结合了model_name/model.id-model.updated_at。对于 Post 模型来说,cache_key形式将是:post/123-201312121212

什么时候使用 fresh_when 和 stale?方法,它们之间有什么区别?

若你有特定的响应处理(如下面代码中的 show 动作),请使用 stale?方法;若你没有特定的响应处理,例如你不需要使用 respond_to 或调用 render 方法(如下面代码中的 edit 和 recent 动作),请使用 fresh_when。

自定义 ETag 的生成 (请注意:该小节的示例代码只是用来说明如何自定义,实际使用中请勿模仿!)

假如你缓存是基于 current_user 或 current_customer,可以通过传入 Hash 格式的参数来生成自己的 ETag。 示例代码如下:

class PostsController < ApplicationController
  def show
    @post = Post. find(params[:id])
    if stale? @post, current_user_id: current_user.id
      respond_to do |format|
        format.html # show.html.erb
        format.json { render json: @post }
      end
    end
  end

  def edit
    @post = Post.find(params[:id])
    fresh_when @post, current_user_id: current_user.id
  end

  def recent
    @post = Post.find(params[:id])
    fresh_when @post, current_user_id: current_user.id
  end

end

Rails 4 中的声明式 ETag 特性

聪明的你一定发现,以上一些代码中有些代码冗余,我们要 DRY!这里就是 Rails4 引入的 ETags 声明特性发挥威力的时候了。 Rails 3 和 Rails 4 都默认使用 ETags 机制处理浏览器缓存。但 Rails 4 添加了声明式 ETags 特性,该特性允许你在控制器中添加全局的 Etag 信息。 示例代码:

class PostsController < ApplicationController
etag { current_user.id }
  def show
    @post = Post. find(params[:id])
    if stale? @post
      respond_to do |format|
        format.html # show.html.erb
        format.json { render json: @post }
      end
    end
  end

  def edit
    @post = Post.find(params[:id])
    fresh_when @post
  end

  def recent
    @post = Post.find(params[:id])
    fresh_when @post
  end

end

你可以生成多个 ETags:

class PostsController < ApplicationController
  etag { current_user.id }
  etag { current_customer.id }
  # ...
end

你也能够设置 ETags 的生成条件:

class PostsController < ApplicationController
  etag do
    { current_user_id: current_user.id } if %w(show edit).include? params[:action]
  end
end

注意:声明式 ETags 并不支持:only,:if参数选项

翻译自:Browser Cache: How ETags Works in Rails 3 and Rails 4

赞。

有一个小问题,

并根据生成的响应内容生成 MD5 加密的 ETag

MD5 不是加密,所以这里应该是 MD5 散列

#4 楼 @qsun 谢谢指正,已修改。不过有点不太明白,网上一些资料大多都将加密与 MD5 联系起来,是否可以帮我解释一下,谢谢 😄 Wiki

MD5 訊息摘要演算法(英语:MD5 Message-Digest Algorithm),一種被廣泛使用的密碼雜湊函數,可以產生出一個 128 位元(16 位元組)的散列值(hash value),用于确保信息传输完整一致。

#5 楼 @huopo125

hash 杂凑函数 或者 散列函数 是不可逆的(在密码学里面的 hash 函数,基本上不但是不可逆的,而且是对于冲突有抗性 - 也就是给定一个输入和输出的 pair,很难找到另外一个不同的输入可以生成相同的输出)。而加密的话需要是可逆的。

几个例子: 对于 etag 而言,目标是:如果两段内容内容不同,需要生成不同的特征码,所以使用 hash。 md5/sha1 等等都可以 对于保存的密码,也是一样,需要验证密码是否符合,所以也使用 hash。如果是用加密的话,那就是作死的节奏。 那么如果我写一封邮件给你,不想 NSA 看,但是我又希望你看见内容,那么我就发送一封 “加密” 邮件给你。如果我是发送的 hash 给你,除非我们有预先预定,否则你也看不懂。

希望这几个例子能帮助你轻松理解。


写到这里我突然想起来个事情, bcrypt & scrypt 都是杂凑函数,而非加密算法。

#6 楼 @qsun 明白了,非常感谢

有一个小问题想问一下: 关于自定义 etag 的例子,是把 current_user.id 作为 cache_key, 这样做的含义是当是同一个用户的请求时,请返回 304,然后 broswer 使用自己的缓存。 是不是我的理解环节是有问题的,感觉这样做行不通啊,谢谢

#8 楼 @johnnyhappy365 你的理解是对的,谢谢指出。文章中的示例只是简单说了一下如何用,实际中不会这么做,可以参考 ruby-china 源码中的 key 设计: fresh_when(etag: [@topic, @has_followed, @has_favorited, @replies, @node, @show_raw])

#9 楼 @huopo125 嗯,这个例子更生动一些。

帖子很好。不过这种玩法确实把代码搞得很难看。有更高级的玩法吗?我所知道的就是 http://meteor.com 的实时 WEB 是一种解决方案,本地数据缓存加数据变动时自动消息推送更新。

涨姿势,呵呵

👍,很好的总结

这个是 Rack 中 ETag 的相关代码:

digest, body = digest_body(body)
headers['ETag'] = %("#{digest}") if digest
def digest_body(body)
        parts = []
        body.each { |part| parts << part }
        string_body = parts.join
        digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
        [digest, parts]
end

ETag 是根据 body 来生成的,有个问题:如果数据是来源于外部系统的response,那么是不是需要对数据排序才能确保ETag的准确性?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号