首先 Page Caching 在 Rails 4 已经被移除作为 Gem actionpack-page_caching
存在了。
因为 DHH 在后来要推行他的 key-based cache
实践,事实证明这样的做法更灵活更好。
回到 actionpack-page_caching
的讨论中
把对应需要缓存的 action 内容,在 public 文件夹中缓存一份地址对应的 html 文件,让 nginx 直接访问这个 html 文件,那 Rails Application 就不会参与任何的处理。
# 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
首先
action_controller/page_caching.rb
让所有的 Controller 都 includeActionController::Caching::Pages
这个 Module。 然后在 caches_pages 被声明的 action 就会走一个 after_filter 执行 cache_page 这一个实例方法 最后得到 response.body,写进 public 里头的对应目录文件里,下次访问这个路由 Nginx 会直接访问这个文件,应用服务器将不参与任何处理。
优点: 快,适合变化频率低的数据。
缺点: 缓存失效的策略只有一个:手动 expire_page。所以 2012 年就被 DHH 逐出 Rails 4 的核心代码。
Action Caching 也在 Rails 4 中移除作为 Gem actionpack-action_caching
存在了。
乍一看以为 Action Caching 对比 Page Caching 是新瓶装旧药,但是两者的缓存上的策略和思路是不同的。Action Caching 实现了一个 around_action 的方法,由于是走方法的,所以可以提供「unless」的保存规则。
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
用 around_action 来实现是否进入 action 原有的逻辑代码还是直接访问缓存文件,这一点算是 Page Caching 的强化版本。
如果有缓存,将会直接从 read_fragment 中读取内容,然后直接作为 response_body 返回,如果不,就执行 yield 中代码(本身 action 的代码),然后_save_fragment。
和 Page Caching 不同,他最终的缓存访问这个行为会走进 Rails App 的 action 里,所以这个是缺点(不是直接访问文件),也是优点(不会无脑访问文件)。
但是这种在 action 层面上的缓存还是太不自由了,基本上是同时被逐出 Rails 4。
前面两位被弃用的主要原因,就是现在 view 方面的需求太复杂了,一步到位的缓存是不存在的。
于是我们就有了在 view 层面上碎片化的 Key-base 缓存方案:Fragment Caching。
在 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 提供的。
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 如何读写保存碎片的部分,步兵地址摸我。
事实上从 Key 的内容我们就可以知道,这个以 action + model base attribute + 自定义字符串的玩法,对于现在的 Rails 项目来说再好不过了。 更加碎片自定义的信息,更高而且准确的缓存命中,但是坏处非常非常明显,下一部分说。
缓存从一开始就带着开发者对于它可能会发生的「不失效」有着巨大的忧虑。用了 Fragment Caching,在我们不增加其他代码的前提下,意味着我们把缓存的是否失效留给了 updated_at 这个时间戳。在最简单原始的场景下,这倒是万能解决方案。
——直到我们用上了「俄罗斯套娃」缓存,那我们接下来就不得不可能在父子层级的 model 之间加恼人的 touch,然后继续期望缓存会在适当的时候乖乖失效。