Rails Rails 缓存,你应该知道的几件事

chrishyman · 2017年03月12日 · 最后由 chrishyman 回复于 2018年04月20日 · 7783 次阅读
本帖已被管理员设置为精华贴

Page Caching

首先 Page Caching 在 Rails 4 已经被移除作为 Gem actionpack-page_caching 存在了。 因为 DHH 在后来要推行他的 key-based cache 实践,事实证明这样的做法更灵活更好。

回到 actionpack-page_caching 的讨论中

1. 原理:

把对应需要缓存的 action 内容,在 public 文件夹中缓存一份地址对应的 html 文件,让 nginx 直接访问这个 html 文件,那 Rails Application 就不会参与任何的处理。

2. 核心代码 (有删减):

步兵地址

# action_controller/page_caching.rb
ActionController::Base.send(:include, ActionController::Caching::Pages)

# action_controller/caching/pages.rb
module ActionController
  module Caching
    module Pages
      extend ActiveSupport::Concern
      module ClassMethods
        def caches_page(*actions)
          options = actions.extract_options!
          after_filter({only: actions}.merge(options)) do |c|
            c.cache_page(nil, nil, gzip_level)
          end
        end
      end
    end
  end
end

# controller instance method
def cache_page(content = nil, options = nil, gzip = Zlib::BEST_COMPRESSION)
  if (type = Mime::LOOKUP[self.content_type]) && (type_symbol = type.symbol).present?
    extension = ".#{type_symbol}"
  end
  self.class.cache_page(content || response.body, path, extension, gzip)
end

3. 解说:

首先 action_controller/page_caching.rb 让所有的 Controller 都 include ActionController::Caching::Pages 这个 Module。 然后在 caches_pages 被声明的 action 就会走一个 after_filter 执行 cache_page 这一个实例方法 最后得到 response.body,写进 public 里头的对应目录文件里,下次访问这个路由 Nginx 会直接访问这个文件,应用服务器将不参与任何处理。

4. 优缺点

优点: 快,适合变化频率低的数据。

缺点: 缓存失效的策略只有一个:手动 expire_page。所以 2012 年就被 DHH 逐出 Rails 4 的核心代码。

Action Caching

Action Caching 也在 Rails 4 中移除作为 Gem actionpack-action_caching 存在了。

1. 原理:

乍一看以为 Action Caching 对比 Page Caching 是新瓶装旧药,但是两者的缓存上的策略和思路是不同的。Action Caching 实现了一个 around_action 的方法,由于是走方法的,所以可以提供「unless」的保存规则。

2. 核心代码 (有删减):

步兵地址

module ClassMethods
  def caches_action(*actions)
    return unless cache_configured?
    # cache_options 来源有删减
    around_action ActionCacheFilter.new(cache_options), filter_options
  end
end
protected
class ActionCacheFilter
  # before_around 将会执行的部分
  def around(controller)
    cache_layout = expand_option(controller, @cache_layout)
    path_options = expand_option(controller, @cache_path)
    cache_path = ActionCachePath.new(controller, path_options || {})
    body = controller.read_fragment(cache_path.path, @store_options)
    unless body
      controller.action_has_layout = false unless cache_layout
      yield
      controller.action_has_layout = true
      body = controller._save_fragment(cache_path.path, @store_options)
    end
    body = render_to_string(controller, body) unless cache_layout
    controller.response_body = body
    controller.content_type = Mime[cache_path.extension || :html]
  end

3. 解说:

用 around_action 来实现是否进入 action 原有的逻辑代码还是直接访问缓存文件,这一点算是 Page Caching 的强化版本。

如果有缓存,将会直接从 read_fragment 中读取内容,然后直接作为 response_body 返回,如果不,就执行 yield 中代码(本身 action 的代码),然后_save_fragment。

4. 优缺点

和 Page Caching 不同,他最终的缓存访问这个行为会走进 Rails App 的 action 里,所以这个是缺点(不是直接访问文件),也是优点(不会无脑访问文件)。

但是这种在 action 层面上的缓存还是太不自由了,基本上是同时被逐出 Rails 4。

Fragment Caching

前面两位被弃用的主要原因,就是现在 view 方面的需求太复杂了,一步到位的缓存是不存在的。

于是我们就有了在 view 层面上碎片化的 Key-base 缓存方案:Fragment Caching。

1. 原理:

在 View 中用的 cache 方法来自 ActionView::Helpers::CacheHelper,例子如下: 如果我们在 View 中写下如下代码

<% cache @article do %>
  <%= @article.title %>
  <%= @article.content %>
<% end %>

_ 我们会往我们的缓存抽象层(cache_store)保存如下地址的 key,内容当然是我们的渲染内容。

views/articles/2-20160829091941000000000/6b45efaa1f600a3b9dffe061fb38ad68
     ^类名     ^article  id-updated_at  ^ActionView::Digestor 的digest 方法生成的特征码

值得一说的是,这个虽然是 ActionView 的方法,但是 read_fragment 和 write_fragment 都是 ActionController::Base 提供的。

2. 核心代码 (有删减):

步兵地址

def cache(name = {}, options = nil, &block)
  # 略了若干判断,以及TextHelper 的 safe_concat 方法
  # cache_fragment_name(name, options) 就是我们的存储key 来源的方法 TL;DR
  fragment_for(cache_fragment_name(name, options), options, &block)
end

def fragment_for(name = {}, options = nil, &block)
  read_fragment_for(name, options) || write_fragment_for(name, options, &block)
end

def read_fragment_for(name, options)
  controller.read_fragment(name, options)
end

def write_fragment_for(name, options)
  pos = output_buffer.length
  yield
  output_safe = output_buffer.html_safe?
  fragment = output_buffer.slice!(pos..-1)
  if output_safe
    self.output_buffer = output_buffer.class.new(output_buffer)
  end
  controller.write_fragment(name, fragment, options)
end

关于 ActionController 如何读写保存碎片的部分,步兵地址摸我

3. 解说:

事实上从 Key 的内容我们就可以知道,这个以 action + model base attribute + 自定义字符串的玩法,对于现在的 Rails 项目来说再好不过了。 更加碎片自定义的信息,更高而且准确的缓存命中,但是坏处非常非常明显,下一部分说。

4. 优缺点:

缓存从一开始就带着开发者对于它可能会发生的「不失效」有着巨大的忧虑。用了 Fragment Caching,在我们不增加其他代码的前提下,意味着我们把缓存的是否失效留给了 updated_at 这个时间戳。在最简单原始的场景下,这倒是万能解决方案。

——直到我们用上了「俄罗斯套娃」缓存,那我们接下来就不得不可能在父子层级的 model 之间加恼人的 touch,然后继续期望缓存会在适当的时候乖乖失效。

SQL Caching

施工中,不日更新
huacnlee 将本帖设为了精华贴。 03月12日 21:05

不应该不知道 == 应该知道?感觉前者说的好绕

small_fish__ 回复

「双重否定表示肯定」(重点错😂

这个 beauti 好绕口

标题绕口。

@easonlovewan @Rei 为什么重点都是这个😓

好贴,最近公司有一个缓存任务,正好看看,现在遇到一个缓存不失效的问题。是不是可以把每次 update 这个 modle 的时候,更新下缓存?

@ForrestDouble 论坛里面还有一篇讲的也挺细致的。你可以看看。 https://ruby-china.org/topics/21488

ForrestDouble 回复

如果 View 上面实现的是俄罗斯套娃的话,那么就必须往上层 touch 了,如果担心性能问题可以考虑 after_save 之后选择性 touch(尽管代码复杂度会变复杂),缓存本身是不能更新的(实现层面),只能失效。 如果是默认的 CacheHelper 的 cache 方法,索引的 key 是 ActionController 的 fragment_cache_key 方法实现的,最后的结果会是

ActiveSupport::Cache.expand_cache_key([你的 cache 里头的数组或者 model,或者 hash])

PS: 另外说下,如果你的 cache key 里头丢的是 hash

hash = {a: 1, b: 2}

ActiveSupport::Cache.expand_cache_key hash

# 你会得到 a/1/b/2

”写进public 里头的对应目录文件里,下次访问这个路由 Nginx 会直接访问这个文件,应用服务器将不参与任何处理。“

这句话看的我这个新手好慌,Nginx 是怎么知道这一切的?是怎么知道“下次直接访问”的?我好焦虑。

page cache 功能的实现需要 nginx 配合,在 nginx 的配置文件中增加路由匹配。示例如下:https://gist.github.com/YanhaoYang/5641abe7ab5630a9cebb

实际上 nginx 每次都会通过 try_files 直接访问这个文件,只是当发现没有这个文件时,会将请求转发给 Rails,如果这个文件存在,则会直接返回,Rails 就不会参与本次请求。

焦虑一下子释放了。

early 回复

😂 是这样哒。

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