Rails自带缓存

945 阅读5分钟

缓存是指存储请求-响应循环中生成的内容,在类似请求的响应中复用

1 基本缓存

默认情况下,缓存只在生产环境启用。如果想在本地启用缓存,要在相应的 config/environments/*.rb 文件中把 config.action_controller.perform_caching 设为 true。

1.1 片段缓存(view)

动态 Web 应用一般使用不同的组件构建页面,不是所有组件都能使用同一种缓存机制。如果页面的不同部分需要使用不同的缓存机制,在不同的条件下失效,可以使用片段缓存。

片段缓存把视图逻辑的一部分放在 cache 块中,下次请求使用缓存存储器中的副本伺服。

例如,如果想缓存页面中的各个商品,可以使用下述代码:

  <% @products.each do |product| %>
    <% cache product do %>
      <%= render product %>
    <% end %>
  <% end %>

首次访问这个页面时,Rails 会创建一个具有唯一键的缓存条目。缓存键类似下面这种:

views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901

中间的数字是 product_id 加上商品记录的 updated_at 属性中存储的时间戳。Rails 使用时间戳确保不伺服过期的数据。如果 updated_at 的值变了,Rails 会生成一个新键,然后在那个键上写入一个新缓存,旧键上的旧缓存不再使用。这叫基于键的失效方式。

视图片段有变化时(例如视图的 HTML 有变),缓存的片段也失效。缓存键末尾那个字符串是模板树摘要,是基于缓存的视图片段的内容计算的 MD5 哈希值。如果视图片段有变化,MD5 哈希值就变了,因此现有文件失效。

如果想在特定条件下缓存一个片段,可以使用 cache_if 或 cache_unless:

<% cache_if admin?, product do %>
  <%= render product %>
<% end %>

有时,可能想把缓存的片段嵌套在其他缓存的片段里。这叫俄罗斯套娃缓存(Russian doll caching)。

俄罗斯套娃缓存的优点是,更新单个商品后,重新生成外层片段时,其他内存片段可以复用。

如果model之间互相有一对多或是多对多的关系时:

class Project < ApplicationRecord
  has_many :todolists
  has_many :todos
end

class Todolist < ApplicationRecord
  belongs_to :project, touch: true
end

class Todo < ApplicationRecord
  belongs_to :project, touch: true
end

我们在子model的belongs_to后面都加上的touch: true,这个参数的作用是在每次更新todo和todolist的时候,会把它对应的那条belongs_to的project的updated_at也一并更新,这样在partial中使用todos和todolists时,依然会是最新的数据,而不会有更新了todo没更新project,导致缓存未失效的情况。

还可以嵌套:

- cache @project do
  = @project.name
  - cache [:todolists, @todolists.max(&:updated_at)] do
    = render partial: 'todolists/todolist', collection: @todolists
  - cache [:todos, @todos.max(&:updated_at)] do
    = render partial: 'todos/todo', collection: @todo

cache_key生成的方式做了改变,传的是一个数组,这样生成的cache_key是数组中的各个元素加起来计算的结果,这里是以字符和todos中最新的更新时间来生成的,只要有更新,最新的那个一定会变化,而加上前面字符的原因,是因为当@todos为nil的时候,生成的cache_key依然有自己的标识,不会与@todolists生成的cache_key混乱。

- cache [:todos, @todos.max(&:updated_at)] do
  @project.name
  = render partial: 'todos/todo', collection: @todos

project.name是显示在todolist这个partial这部分的时候,我们只更新project,而没有更新它has_many的todos,那这条缓存依旧不会失效,渲染的时候还是之前的name。这是一个比较常见的错误,基本比较大众的解决办法都是把cache_key由[:todos, @todos.max(&:updated_at)]改成[:todos, @project.name, @todos.max(&:updated_at)],通过cache_key的生成方式来改变,如果这条缓存里面展示的project的内容较多的话,cache_key可以直接改成[:todos, @project, @todos.max(&:updated_at)],不过这样有可能会降低缓存的命中率,因为有touch: true,如果说project的has_many比较多,更改其他内容很会带动project的updated_at更新,孰优孰劣,看情况选择。

1.1.1 集合缓存

render 辅助方法还能缓存渲染集合的单个模板。这甚至比使用 each 的前述示例更好,因为是一次性读取所有缓存模板的,而不是一次读取一个。若想缓存集合,渲染集合时传入 cached: true 选项:

<%= render partial: 'products/product', collection: @products, cached: true %>

上述代码中所有的缓存模板一次性获取,速度更快。此外,尚未缓存的模板也会写入缓存,在下次渲染时获取。

1.2 低层缓存(model)

有时需要缓存特定的值或查询结果,而不是缓存视图片段。Rails 的缓存机制能存储任何类型的信息。

实现低层缓存最有效的方式是使用 Rails.cache.fetch 方法。这个方法既能读取也能写入缓存。传入单个参数时,获取指定的键,返回缓存中的值。如果传入块,块中的代码在缓存缺失时执行。块返回的值将写入缓存,存在指定键的名下,然后返回那个返回值。如果命中缓存,直接返回缓存的值,而不执行块中的代码。

下面举个例子。应用中有个 Product 模型,它有个实例方法,在竞争网站中查找商品的价格。这个方法返回的数据特别适合使用低层缓存:

class Product < ApplicationRecord
  def competing_price
    Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
      Competitor::API.find_price(id)
    end
  end
end

注意,这个示例使用了 cache_key 方法,因此得到的缓存键类似这种:products/233-20140225082222765838000/competing_price。cache_key 方法根据模型的 id 和 updated_at 属性生成一个字符串。这是常见的约定,有个好处是,商品更新后缓存自动失效。一般来说,使用低层缓存缓存实例层信息时,需要生成缓存键。