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

chrishyman · 发布于 2017年3月12日 · 最后由 sunsunsun 回复于 2017年3月22日 · 2020 次阅读
930
本帖已被设为精华帖!

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

施工中, 不日更新
共收到 9 条回复
2 huacnlee 将本帖设为了精华贴 3月12日 21:05
2973

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

930
2973small_fish__ 回复

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

17671

这个beauti好绕口

1

标题绕口。

930

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

28931

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

20361

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

930
28931ForrestDouble 回复

如果 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
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册