更多内容在 rubyonrails.org:

Rails 缓存概述

本指南介绍了如何使用缓存来加速 Rails 应用程序。

缓存是指在请求-响应周期中存储生成的內容,并在响应类似请求时重复使用。

缓存通常是提高应用程序性能的最有效方法。通过缓存,运行在单台服务器和单一数据库上的网站可以承受数千个并发用户的负载。

Rails 开箱即用地提供了一组缓存功能。本指南将教你了解每种缓存功能的作用域和目的。掌握这些技巧,你的 Rails 应用程序就可以处理数百万个视图,而不会出现过高的响应时间或服务器费用。

阅读完本指南后,你将了解

  • 片段缓存和俄罗斯套娃缓存。
  • 如何管理缓存依赖项。
  • 替代缓存存储。
  • 条件 GET 支持。

1 基本缓存

这是对三种类型缓存技术的介绍:页面缓存、动作缓存和片段缓存。默认情况下,Rails 提供片段缓存。要使用页面缓存和动作缓存,你需要在 Gemfile 中添加 actionpack-page_cachingactionpack-action_caching

默认情况下,Action Controller 缓存仅在生产环境中启用。你可以在本地运行 rails dev:cache 或在 config/environments/development.rb 中将 config.action_controller.perform_caching 设置为 true 来测试本地缓存。

更改 config.action_controller.perform_caching 的值只会影响 Action Controller 提供的缓存。例如,它不会影响我们将在 下面 讨论的底层缓存。

1.1 页面缓存

页面缓存是 Rails 中的一种机制,它允许由 Web 服务器(例如 Apache 或 NGINX)直接处理生成的页面的请求,而无需经过整个 Rails 堆栈。虽然这非常快,但它不能应用于所有情况(例如需要身份验证的页面)。此外,由于 Web 服务器直接从文件系统提供文件,你需要实现缓存过期。

页面缓存已从 Rails 4 中移除。请查看 actionpack-page_caching gem

1.2 动作缓存

页面缓存不能用于具有前置过滤器(例如,需要身份验证的页面)的动作。这就是动作缓存的作用。动作缓存的工作原理与页面缓存类似,只是传入的 Web 请求会命中 Rails 堆栈,以便在提供缓存之前运行前置过滤器。这允许在提供缓存结果的同时运行身份验证和其他限制。

动作缓存已从 Rails 4 中移除。请查看 actionpack-action_caching gem。有关新方法的更多信息,请查看 DHH 的基于键的缓存过期概述

1.3 片段缓存

动态 Web 应用程序通常使用各种组件构建页面,这些组件并非都具有相同的缓存特性。当页面不同部分需要分别缓存和过期时,你可以使用片段缓存。

片段缓存允许将视图逻辑片段包装在缓存块中,并在下一个请求到达时从缓存存储中提供。

例如,如果你想缓存页面上的每个产品,你可以使用以下代码

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

当你的应用程序接收到对该页面的第一个请求时,Rails 将使用一个唯一的键写入一个新的缓存条目。键类似于以下形式

views/products/index:bea67108094918eeba42cd4a6e786901/products/1

中间的字符串是一串字符,它是视图片段模板树摘要。它是根据你正在缓存的视图片段的內容计算出的哈希摘要。如果你更改了视图片段(例如,HTML 更改),摘要将发生更改,从而使现有文件过期。

缓存版本(从产品记录中得出)存储在缓存条目中。当产品被触碰时,缓存版本将发生更改,任何包含先前版本的缓存片段将被忽略。

像 Memcached 这样的缓存存储会自动删除旧的缓存文件。

如果你想在某些条件下缓存片段,可以使用 cache_ifcache_unless

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

1.3.1 集合缓存

render 助手还可以缓存为集合渲染的单个模板。它甚至可以比使用 each 的前一个示例更上一层楼,一次读取所有缓存模板,而不是一个接一个地读取。这是通过在渲染集合时传递 cached: true 来实现的

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

所有来自先前渲染的缓存模板将被一次性获取,速度更快。此外,尚未缓存的模板将被写入缓存,并在下次渲染时进行多重获取。

缓存键可以配置。在下面的示例中,它以当前语言环境为前缀,以确保产品页面的不同语言环境不会相互覆盖

<%= render partial: 'products/product',
           collection: @products,
           cached: ->(product) { [I18n.locale, product] } %>

1.4 俄罗斯套娃缓存

你可能想要将缓存片段嵌套在其他缓存片段中。这称为俄罗斯套娃缓存。

俄罗斯套娃缓存的优点是,如果单个产品更新,则在重新生成外部片段时可以重复使用所有其他内部片段。

如前一节所述,如果直接依赖于缓存文件记录的 updated_at 值发生更改,则缓存文件将过期。但是,这不会使片段嵌套的任何缓存过期。

例如,以下视图

<% cache product do %>
  <%= render product.games %>
<% end %>

反过来渲染了以下视图

<% cache game do %>
  <%= render game %>
<% end %>

如果游戏记录的任何属性发生更改,updated_at 值将设置为当前时间,从而使缓存过期。但是,由于产品对象的 updated_at 不会更改,因此该缓存不会过期,你的应用程序将提供过期的數據。要解决这个问题,我们将使用 touch 方法将模型绑定在一起

class Product < ApplicationRecord
  has_many :games
end

class Game < ApplicationRecord
  belongs_to :product, touch: true
end

使用 touch 设置为 true,任何更改游戏记录 updated_at 的操作也会更改关联产品的 updated_at,从而使缓存过期。

1.5 共享局部缓存

可以共享具有不同 MIME 类型的文件的局部文件和关联缓存。例如,共享局部缓存允许模板编写者在 HTML 和 JavaScript 文件之间共享局部文件。当模板在模板解析器中收集时,它们的文件路径只包含模板语言扩展名,而不包含 MIME 类型。因此,模板可以用于多种 MIME 类型。以下代码将响应 HTML 和 JavaScript 请求

render(partial: "hotels/hotel", collection: @hotels, cached: true)

将加载名为 hotels/hotel.erb 的文件。

另一种选择是在要渲染的局部文件中包含 formats 属性。

render(partial: "hotels/hotel", collection: @hotels, formats: :html, cached: true)

将在任何文件 MIME 类型中加载名为 hotels/hotel.html.erb 的文件,例如,你可以在 JavaScript 文件中包含此局部文件。

1.6 管理依赖项

为了正确地使缓存失效,你需要正确地定义缓存依赖项。Rails 足够智能,可以处理常见情况,因此你无需指定任何内容。但是,有时,当你处理自定义助手时,你需要显式地定义它们。

1.6.1 隐式依赖项

大多数模板依赖项可以从模板本身中的 render 调用中得出。以下是一些 ActionView::Digestor 知道如何解码的 render 调用的示例

render partial: "comments/comment", collection: commentable.comments
render "comments/comments"
render "comments/comments"
render("comments/comments")

render "header" # translates to render("comments/header")

render(@topic)         # translates to render("topics/topic")
render(topics)         # translates to render("topics/topic")
render(message.topics) # translates to render("topics/topic")

另一方面,一些调用需要更改才能使缓存正常工作。例如,如果你传递了一个自定义集合,你需要将

render @project.documents.where(published: true)

更改为

render partial: "documents/document", collection: @project.documents.where(published: true)

1.6.2 显式依赖项

有时,你将遇到根本无法推断的模板依赖项。这通常发生在助手内进行渲染时。以下是一个示例

<%= render_sortable_todolists @project.todolists %>

您需要使用特殊的注释格式来调用它们

<%# Template Dependency: todolists/todolist %>
<%= render_sortable_todolists @project.todolists %>

在某些情况下,例如单表继承设置,您可能会有很多显式的依赖项。与其编写每个模板,不如使用通配符匹配目录中的任何模板

<%# Template Dependency: events/* %>
<%= render_categorizable_events @person.events %>

至于集合缓存,如果部分模板没有以干净的缓存调用开头,您仍然可以通过在模板中的任何位置添加特殊的注释格式来从集合缓存中获益,例如

<%# Template Collection: notification %>
<% my_helper_that_calls_cache(some_arg, notification) do %>
  <%= notification.name %>
<% end %>

1.6.3 外部依赖项

如果您使用辅助方法(例如,在缓存块内),然后更新该辅助方法,则您也必须更新缓存。您如何操作并不重要,但模板文件的 MD5 必须更改。一个建议是简单地在注释中明确说明,例如

<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
<%= some_helper_method(person) %>

1.7 低级缓存

有时您需要缓存特定值或查询结果,而不是缓存视图片段。Rails 的缓存机制非常适合存储任何可序列化的信息。

实现低级缓存最有效的方式是使用Rails.cache.fetch方法。此方法执行对缓存的读写操作。当只传递一个参数时,将获取键并返回缓存中的值。如果传递一个块,则该块将在发生缓存未命中时执行。块的返回值将被写入给定缓存键下的缓存,并且该返回值将被返回。如果发生缓存命中,则将返回缓存的值,而不会执行块。

考虑以下示例。应用程序有一个Product模型,它有一个实例方法,该方法在竞争网站上查找产品的价格。此方法返回的数据非常适合低级缓存

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

请注意,在这个例子中我们使用了cache_key_with_version方法,因此生成的缓存键将类似于products/233-20140225082222765838000/competing_pricecache_key_with_version根据模型的类名、idupdated_at属性生成字符串。这是一个常见的约定,并且具有在产品更新时使缓存无效的优点。一般来说,当您使用低级缓存时,您需要生成缓存键。

1.7.1 避免缓存 Active Record 对象的实例

考虑以下示例,该示例将表示超级用户的 Active Record 对象列表存储在缓存中

# super_admins is an expensive SQL query, so don't run it too often
Rails.cache.fetch("super_admin_users", expires_in: 12.hours) do
  User.super_admins.to_a
end

你应该避免这种模式。为什么?因为实例可能会发生变化。在生产环境中,它的属性可能会不同,或者记录可能会被删除。在开发环境中,它与在您进行更改时重新加载代码的缓存存储一起不可靠地工作。

相反,缓存 ID 或其他原始数据类型。例如

# super_admins is an expensive SQL query, so don't run it too often
ids = Rails.cache.fetch("super_admin_user_ids", expires_in: 12.hours) do
  User.super_admins.pluck(:id)
end
User.where(id: ids).to_a

1.8 SQL 缓存

查询缓存是 Rails 的一项功能,它缓存每个查询返回的结果集。如果 Rails 在同一个请求中再次遇到相同的查询,它将使用缓存的结果集,而不是再次对数据库运行查询。

例如

class ProductsController < ApplicationController
  def index
    # Run a find query
    @products = Product.all

    # ...

    # Run the same query again
    @products = Product.all
  end
end

第二次对数据库运行相同的查询时,实际上它不会命中数据库。第一次从查询返回结果时,它会被存储在查询缓存(在内存中),第二次从内存中提取。

但是,重要的是要注意查询缓存是在操作开始时创建并在操作结束时销毁的,因此仅在操作持续时间内存在。如果您想以更持久的方式存储查询结果,可以使用低级缓存。

2 缓存存储

Rails 为缓存数据提供了不同的存储(除了 SQL 和页面缓存)。

2.1 配置

您可以通过设置config.cache_store配置选项来设置应用程序的默认缓存存储。其他参数可以作为参数传递给缓存存储的构造函数

config.cache_store = :memory_store, { size: 64.megabytes }

或者,您可以在配置块之外设置ActionController::Base.cache_store

您可以通过调用Rails.cache来访问缓存。

2.1.1 连接池选项

默认情况下,:mem_cache_store:redis_cache_store被配置为使用连接池。这意味着如果您使用 Puma 或其他线程服务器,您可以让多个线程同时对缓存存储执行查询。

如果您想禁用连接池,在配置缓存存储时将:pool选项设置为false

config.cache_store = :mem_cache_store, "cache.example.com", { pool: false }

您还可以通过向:pool选项提供单独的选项来覆盖默认的池设置

config.cache_store = :mem_cache_store, "cache.example.com", { pool: { size: 32, timeout: 1 } }
  • :size - 此选项设置每个进程的连接数(默认值为 5)。

  • :timeout - 此选项设置等待连接的秒数(默认值为 5)。如果在超时时间内没有连接可用,则将引发Timeout::Error错误。

2.2 ActiveSupport::Cache::Store

ActiveSupport::Cache::Store 为与 Rails 中的缓存交互提供了基础。这是一个抽象类,您不能单独使用它。相反,您必须使用与存储引擎绑定的类的具体实现。Rails 附带了几个实现,如下所述。

主要 API 方法是readwritedeleteexist?fetch.

传递给缓存存储构造函数的选项将被视为相应 API 方法的默认选项。

2.3 ActiveSupport::Cache::MemoryStore

ActiveSupport::Cache::MemoryStore 将条目保存在同一个 Ruby 进程的内存中。缓存存储的大小有限制,由发送给初始化程序的:size选项指定(默认值为 32Mb)。当缓存超过分配的大小限制时,将执行清理,并且最不常使用的条目将被删除。

config.cache_store = :memory_store, { size: 64.megabytes }

如果您运行多个 Ruby on Rails 服务器进程(如果您使用 Phusion Passenger 或 puma 集群模式,情况就是这样),那么您的 Rails 服务器进程实例将无法彼此共享缓存数据。此缓存存储不适合大型应用程序部署。但是,它可以很好地用于小型、流量较小的网站,这些网站只有几个服务器进程,以及开发和测试环境。

默认情况下,新的 Rails 项目配置为在开发环境中使用此实现。

由于进程在使用:memory_store时不会共享缓存数据,因此将无法通过 Rails 控制台手动读取、写入或过期缓存。

2.4 ActiveSupport::Cache::FileStore

ActiveSupport::Cache::FileStore 使用文件系统来存储条目。在初始化缓存时,必须指定存储文件的目录路径。

config.cache_store = :file_store, "/path/to/cache/directory"

使用此缓存存储,同一主机上的多个服务器进程可以共享一个缓存。此缓存存储适用于在同一台或两台主机上提供服务的流量较低或中等的网站。在不同主机上运行的服务器进程可以通过使用共享文件系统来共享一个缓存,但此设置不推荐。

由于缓存将一直增长到磁盘空间用完为止,建议定期清除旧的条目。

这是默认的缓存存储实现(位于"#{root}/tmp/cache/"),如果未提供显式的config.cache_store

2.5 ActiveSupport::Cache::MemCacheStore

ActiveSupport::Cache::MemCacheStore 使用 Danga 的memcached服务器为您的应用程序提供集中式缓存。Rails 默认使用捆绑的dalli gem。这目前是生产网站最流行的缓存存储。它可以用于提供单个、共享的缓存集群,具有非常高的性能和冗余性。

在初始化缓存时,您应该指定集群中所有 memcached 服务器的地址,或者确保MEMCACHE_SERVERS环境变量已正确设置。

config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"

如果两者都没有指定,它将假定 memcached 正在默认端口(127.0.0.1:11211)上的 localhost 上运行,但这对于大型网站来说不是理想的设置。

config.cache_store = :mem_cache_store # Will fallback to $MEMCACHE_SERVERS, then 127.0.0.1:11211

有关支持的地址类型,请参见Dalli::Client 文档

此缓存上的write(和fetch)方法接受其他选项,这些选项利用了 memcached 的特定功能。

2.6 ActiveSupport::Cache::RedisCacheStore

ActiveSupport::Cache::RedisCacheStore 利用 Redis 对达到最大内存时自动驱逐的支持,使其行为更像 Memcached 缓存服务器。

部署说明:Redis 默认情况下不会过期键,因此请务必使用专用的 Redis 缓存服务器。不要用易失性缓存数据填满您的持久性 Redis 服务器!详细阅读Redis 缓存服务器设置指南

对于仅缓存的 Redis 服务器,将maxmemory-policy设置为 allkeys 的变体之一。Redis 4+ 支持最不常使用驱逐(allkeys-lfu),这是一个很好的默认选择。Redis 3 及更早版本应使用最不常使用驱逐(allkeys-lru)。

设置相对较低的缓存读取和写入超时时间。重新生成缓存的值通常比等待一秒以上才能检索它更快。读取和写入超时时间默认设置为 1 秒,但如果您的网络始终处于低延迟状态,可以设置更低的值。

默认情况下,缓存存储会在请求期间连接失败时尝试重新连接到 Redis 一次。

缓存读取和写入永远不会引发异常;它们只返回nil,行为就像缓存中没有任何东西一样。为了评估您的缓存是否正在遇到异常,您可以提供error_handler来向异常收集服务报告。它必须接受三个关键字参数:method,最初调用的缓存存储方法;returning,返回给用户的 value,通常为nil;以及exception,被拯救的异常。

要开始,将 redis gem 添加到 Gemfile 中

gem "redis"

最后,在相关的 config/environments/*.rb 文件中添加配置

config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }

一个更复杂的生产 Redis 缓存存储可能看起来像这样

cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
config.cache_store = :redis_cache_store, { url: cache_servers,

  connect_timeout:    30,  # Defaults to 1 second
  read_timeout:       0.2, # Defaults to 1 second
  write_timeout:      0.2, # Defaults to 1 second
  reconnect_attempts: 2,   # Defaults to 1

  error_handler: -> (method:, returning:, exception:) {
    # Report errors to Sentry as warnings
    Sentry.capture_exception exception, level: "warning",
      tags: { method: method, returning: returning }
  }
}

2.7 ActiveSupport::Cache::NullStore

ActiveSupport::Cache::NullStore 的作用域是每个 Web 请求,并在请求结束时清除存储的值。它旨在用于开发和测试环境。当您有直接与 Rails.cache 交互的代码,但缓存会干扰查看代码更改结果时,它会非常有用。

config.cache_store = :null_store

2.8 自定义缓存存储

您可以通过扩展 ActiveSupport::Cache::Store 并实现相应的方法来创建自己的自定义缓存存储。这样,您就可以将任意数量的缓存技术替换到您的 Rails 应用程序中。

要使用自定义缓存存储,只需将缓存存储设置为您的自定义类的新的实例。

config.cache_store = MyCacheStore.new

3 缓存键

缓存中使用的键可以是响应 cache_keyto_param 的任何对象。如果您需要生成自定义键,可以在您的类上实现 cache_key 方法。ActiveRecord 会根据类名和记录 ID 生成键。

您可以使用哈希和值的数组作为缓存键。

# This is a legal cache key
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])

您在 Rails.cache 上使用的键与实际与存储引擎一起使用的键不同。它们可能会使用命名空间修改或更改以适应技术后端约束。这意味着,例如,您不能使用 Rails.cache 保存值,然后尝试使用 dalli gem 提取它们。但是,您也不必担心超过 memcached 大小限制或违反语法规则。

4 条件 GET 支持

条件 GET 是 HTTP 规范的一项功能,它为 Web 服务器提供了一种方法来告诉浏览器,对 GET 请求的响应自从上次请求以来没有改变,可以安全地从浏览器缓存中提取。

它们通过使用 HTTP_IF_NONE_MATCHHTTP_IF_MODIFIED_SINCE 标头来回传递唯一的內容标识符和內容上次更改的时间戳。如果浏览器发出请求,其中內容标识符 (ETag) 或自上次修改时间戳与服务器的版本匹配,则服务器只需要返回带有未修改状态的空响应。

服务器 (即我们) 有责任查找上次修改时间戳和 if-none-match 标头,并确定是否发送回完整的响应。借助 Rails 中的条件 GET 支持,这是一项相当容易的任务。

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    # If the request is stale according to the given timestamp and etag value
    # (i.e. it needs to be processed again) then execute this block
    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version)
      respond_to do |wants|
        # ... normal response processing
      end
    end

    # If the request is fresh (i.e. it's not modified) then you don't need to do
    # anything. The default render checks for this using the parameters
    # used in the previous call to stale? and will automatically send a
    # :not_modified. So that's it, you're done.
  end
end

除了选项哈希之外,您还可以简单地传入一个模型。Rails 将使用 updated_atcache_key_with_version 方法来设置 last_modifiedetag

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    if stale?(@product)
      respond_to do |wants|
        # ... normal response processing
      end
    end
  end
end

如果您没有任何特殊的响应处理,并且正在使用默认的渲染机制 (即您没有使用 respond_to 或自己调用 render),那么您在 fresh_when 中有一个简单的助手

class ProductsController < ApplicationController
  # This will automatically send back a :not_modified if the request is fresh,
  # and will render the default template (product.*) if it's stale.

  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, etag: @product
  end
end

last_modifiedetag 都设置时,行为会根据 config.action_dispatch.strict_freshness 的值而有所不同。如果设置为 true,则仅考虑 etag,如 RFC 7232 第 6 节中所述。如果设置为 false,则会考虑两者,如果两个条件都满足,则缓存被认为是新鲜的,这是 Rails 的历史行为。

有时我们想要缓存响应,例如永远不会过期的静态页面。为了实现这一点,我们可以使用 http_cache_forever 助手,这样浏览器和代理就会无限期地缓存它。

默认情况下,缓存的响应将是私有的,仅在用户的 Web 浏览器中缓存。要允许代理缓存响应,请设置 public: true 以指示它们可以向所有用户提供缓存的响应。

使用此助手,last_modified 标头设置为 Time.new(2011, 1, 1).utcexpires 标头设置为 100 年。

谨慎使用此方法,因为除非强制清除浏览器缓存,否则浏览器/代理将无法使缓存的响应失效。

class HomeController < ApplicationController
  def index
    http_cache_forever(public: true) do
      render
    end
  end
end

4.1 强 ETag 与弱 ETag

Rails 默认情况下生成弱 ETag。弱 ETag 允许语义上等效的响应具有相同的 ETag,即使它们的正文并不完全匹配。当我们不希望页面因响应正文中的微小更改而重新生成时,这很有用。

弱 ETag 以 W/ 开头,以区别于强 ETag。

W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag
"618bbc92e2d35ea1945008b42799b0e7" → Strong ETag

与弱 ETag 不同,强 ETag 意味着响应应该完全相同,并且逐字节相同。在对大型视频或 PDF 文件进行范围请求时很有用。一些 CDN 仅支持强 ETag,例如 Akamai。如果您绝对需要生成强 ETag,可以按以下方式进行。

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
  end
end

您也可以直接在响应中设置强 ETag。

response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"

5 开发中的缓存

默认情况下,缓存在开发模式下启用,并使用 :memory_store。这并不适用于 Action Controller 缓存,它默认情况下是禁用的。

要启用 Action Controller 缓存,Rails 提供了 bin/rails dev:cache 命令。

$ bin/rails dev:cache
Development mode is now being cached.
$ bin/rails dev:cache
Development mode is no longer being cached.

要禁用缓存,请将 cache_store 设置为 :null_store

6 参考资料



返回顶部