1. 什么是缓存?
缓存意味着在请求-响应周期中存储生成的内容,并在响应类似请求时重用。这就像把你最喜欢的咖啡杯放在桌上而不是厨房橱柜里——你需要它时它就在那里,节省你的时间和精力。
缓存是提高应用程序性能最有效的方法之一。它允许运行在适度基础设施(单个服务器带单个数据库)上的网站支持数千个并发用户。
Rails 提供了一套开箱即用的缓存功能,不仅允许你缓存数据,还能解决缓存过期、缓存依赖和缓存失效等挑战。
本指南将探索 Rails 完善的缓存策略,从片段缓存到 SQL 缓存。通过这些技术,你的 Rails 应用程序可以处理数百万次浏览,同时保持低响应时间和可控的服务器费用。
2. 缓存类型
这是对一些常见缓存类型的介绍。
默认情况下,Action Controller 缓存只在生产环境中启用。你可以在本地运行 bin/rails dev:cache,或者在 config/environments/development.rb 中将 config.action_controller.perform_caching 设置为 true 来尝试本地缓存。
更改 config.action_controller.perform_caching 的值只会影响 Action Controller 提供的缓存。例如,它不会影响我们下面讨论的低级缓存。
2.1. 片段缓存
动态 Web 应用程序通常使用各种组件构建页面,这些组件并非都具有相同的缓存特性。当页面的不同部分需要单独缓存和过期时,可以使用片段缓存。
片段缓存允许将视图逻辑的一个片段包装在缓存块中,并在下一个请求到来时从缓存存储中提供。
例如,如果你想缓存页面上的每个产品,可以使用以下代码:
<% @products.each do |product| %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
当你的应用程序收到对该页面的第一个请求时,Rails 将写入一个带有唯一键的新缓存条目。键看起来像这样:
views/products/index:bea67108094918eeba42cd4a6e786901/products/1
中间的字符串是模板树摘要。它是根据你正在缓存的视图片段的内容计算出的哈希摘要。如果你更改视图片段(例如,HTML 更改),摘要将更改,使现有文件过期。
缓存版本,来源于产品记录,存储在缓存条目中。当产品被“触碰”(touch)时,缓存版本会更改,任何包含旧版本的缓存片段都将被忽略。
像 Memcached 这样的缓存存储会自动删除旧的缓存文件。
如果你想在特定条件下缓存一个片段,可以使用 cache_if 或 cache_unless:
<% cache_if admin?, product do %>
<%= render product %>
<% end %>
2.1.1. 集合缓存
render 辅助方法也可以缓存为集合渲染的单个模板。它甚至可以通过一次性读取所有缓存模板而不是逐个读取,从而超越之前的 each 示例。这是通过在渲染集合时传递 cached: true 来完成的:
<%= render partial: 'products/product', collection: @products, cached: true %>
来自先前渲染的所有缓存模板将以更快的速度一次性获取。此外,尚未缓存的模板将被写入缓存,并在下次渲染时进行多重获取。
缓存键可以配置。在下面的示例中,它以当前语言环境为前缀,以确保产品页面的不同本地化不会相互覆盖:
<%= render partial: 'products/product',
collection: @products,
cached: ->(product) { [I18n.locale, product] } %>
2.2. 俄罗斯套娃式缓存
你可能希望将缓存片段嵌套在其他缓存片段中。这被称为俄罗斯套娃式缓存。
俄罗斯套娃式缓存的优势在于,如果单个产品更新,那么在重新生成外部片段时,所有其他内部片段都可以被重用。
如前一节所述,如果缓存文件直接依赖的记录的 updated_at 值发生变化,则缓存文件将过期。但是,这不会使该片段嵌套在其中的任何缓存过期。
例如,看以下视图:
<% cache product do %>
<%= render product.games %>
<% end %>
这反过来又会渲染这个视图:
<% cache game do %>
<%= render game %>
<% end %>
如果游戏(game)的任何属性发生变化,updated_at 值将被设置为当前时间,从而使缓存过期。然而,由于产品(product)对象的 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,从而使缓存过期。
2.3. 共享局部缓存
可以在不同 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 文件中。
2.4. 使用 Rails.cache 进行低级缓存
有时你需要缓存特定的值或查询结果,而不是缓存视图片段。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_price。cache_key_with_version 根据模型的类名、id 和 updated_at 属性生成一个字符串。这是一个常见的约定,并且具有在产品更新时使缓存失效的好处。通常,当你使用低级缓存时,你需要生成一个缓存键。
下面是更多使用低级缓存的示例:
# Store a value in the cache
Rails.cache.write("greeting", "Hello, world!")
# Retrieve the value from the cache
greeting = Rails.cache.read("greeting")
puts greeting # Output: Hello, world!
# Fetch a value with a block to set a default if it doesn’t exist
welcome_message = Rails.cache.fetch("welcome_message") { "Welcome to Rails!" }
puts welcome_message # Output: Welcome to Rails!
# Delete a value from the cache
Rails.cache.delete("greeting")
2.4.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
2.5. 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
第二次对数据库运行相同的查询时,它实际上不会访问数据库。第一次从查询返回结果时,它会存储在查询缓存(内存中)中,第二次则从内存中提取。但是,每次检索仍然会实例化查询对象的新实例。
查询缓存在操作开始时创建,在操作结束时销毁,因此只在操作期间存在。如果你想以更持久的方式存储查询结果,可以使用低级缓存。
3. 管理依赖项
为了正确地使缓存失效,你需要正确定义缓存依赖项。Rails 足够聪明,可以处理常见情况,因此你无需指定任何内容。然而,有时,例如当你处理自定义辅助方法时,你需要显式定义它们。
3.1. 隐式依赖项
大多数模板依赖项可以从模板本身的 render 调用中派生。以下是一些 ActionView::Digestor 知道如何解码的 render 调用示例:
render partial: "comments/comment", collection: commentable.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)
3.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 %>
3.3. 外部依赖项
如果你在一个缓存块内使用了一个辅助方法,然后更新了该辅助方法,你还需要更新缓存。你如何做到这一点并不重要,但模板文件的 MD5 必须改变。一个建议是简单地在注释中明确说明,例如:
<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
<%= some_helper_method(person) %>
4. Solid Cache
Solid Cache 是一个由数据库支持的 Active Support 缓存存储。它利用现代 SSD(固态硬盘)的速度,提供经济高效、存储容量更大且基础设施更简化的缓存。虽然 SSD 略慢于 RAM,但对于大多数应用程序来说,差异很小。SSD 通过不需要频繁失效来弥补这一点,因为它们可以存储更多数据。因此,平均缓存未命中次数更少,从而实现更快的响应时间。
Solid Cache 采用 FIFO(先进先出)缓存策略,当缓存达到其限制时,首先添加到缓存中的项目将首先被移除。与 LRU(最近最少使用)缓存(它首先移除最近最少访问的项目,更好地优化常用数据)相比,这种方法更简单但效率较低。然而,Solid Cache 通过允许缓存存活更长时间来弥补 FIFO 较低的效率,从而减少失效的频率。
从 Rails 8.0 版本开始,Solid Cache 默认启用。但是,如果你不想使用它,可以跳过 Solid Cache:
rails new app_name --skip-solid
使用 --skip-solid 标志会跳过 Solid Trifecta 的所有部分(Solid Cache、Solid Queue 和 Solid Cable)。如果你仍然想使用其中一些,可以单独安装它们。例如,如果你想使用 Solid Queue 和 Solid Cable 而不使用 Solid Cache,你可以按照 Solid Queue 和 Solid Cable 的安装指南进行操作。
4.1. 配置数据库
要使用 Solid Cache,你可以在 config/database.yml 文件中配置数据库连接。这是一个 SQLite 数据库的示例配置:
production:
primary:
<<: *default
database: storage/production.sqlite3
cache:
<<: *default
database: storage/production_cache.sqlite3
migrations_paths: db/cache_migrate
在此配置中,cache 数据库用于存储缓存数据。如果需要,你还可以指定不同的数据库适配器,例如 MySQL 或 PostgreSQL。
production:
primary: &primary_production
<<: *default
database: app_production
username: app
password: <%= ENV["APP_DATABASE_PASSWORD"] %>
cache:
<<: *primary_production
database: app_production_cache
migrations_paths: db/cache_migrate
如果在缓存配置中未指定 database 或 databases,Solid Cache 将使用 ActiveRecord::Base 连接池。这意味着缓存读写将成为任何包装数据库事务的一部分。
在生产环境中,缓存存储默认配置为使用 Solid Cache 存储:
# config/environments/production.rb
config.cache_store = :solid_cache_store
你可以通过调用 Rails.cache 来访问缓存:
4.2. 自定义缓存存储
Solid Cache 可以通过 config/cache.yml 文件进行定制:
default: &default
store_options:
# Cap age of oldest cache entry to fulfill retention policies
max_age: <%= 60.days.to_i %>
max_size: <%= 256.megabytes %>
namespace: <%= Rails.env %>
有关 store_options 的完整键列表,请参阅 缓存配置。
在这里,你可以调整 max_age 和 max_size 选项来控制缓存条目的年龄和大小。
4.3. 处理缓存过期
Solid Cache 通过每次写入时递增计数器来跟踪缓存写入。当计数器达到 缓存配置 中 expiry_batch_size 的 50% 时,会触发一个后台任务来处理缓存过期。这种方法确保当缓存需要缩小时,缓存记录过期速度快于写入速度。
后台任务只在有写入时运行,因此当缓存未更新时,该进程处于空闲状态。如果你希望在后台作业而不是线程中运行过期进程,请将缓存配置中的 expiry_method 设置为 :job。
4.4. 分片缓存
如果你需要更高的可伸缩性,Solid Cache 支持分片——将缓存分散到多个数据库中。这会分散负载,使你的缓存更加强大。要启用分片,请在 database.yml 中添加多个缓存数据库:
# config/database.yml
production:
cache_shard1:
database: cache1_production
host: cache1-db
cache_shard2:
database: cache2_production
host: cache2-db
cache_shard3:
database: cache3_production
host: cache3-db
此外,你必须在缓存配置中指定分片:
# config/cache.yml
production:
databases: [cache_shard1, cache_shard2, cache_shard3]
4.5. 加密
Solid Cache 支持加密以保护敏感数据。要启用加密,请在缓存配置中设置 encrypt 值:
# config/cache.yml
production:
encrypt: true
你需要设置你的应用程序以使用Active Record 加密。
4.6. 开发环境中的缓存
默认情况下,在开发模式下启用缓存,使用 :memory_store。这不适用于 Action Controller 缓存,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.
如果你想在开发环境中使用 Solid Cache,请在 config/environments/development.rb 中设置 cache_store 配置:
config.cache_store = :solid_cache_store
并确保创建并迁移了 cache 数据库:
development:
<<: * default
database: cache
要禁用缓存,请将 cache_store 设置为 :null_store:
5. 其他缓存存储
Rails 为缓存数据提供了不同的存储(SQL 缓存除外)。
5.1. 配置
你可以通过设置 config.cache_store 配置选项来设置不同的缓存存储。其他参数可以作为缓存存储构造函数的参数传入:
config.cache_store = :memory_store, { size: 64.megabytes }
或者,你可以在配置块之外设置 ActionController::Base.cache_store。
你可以通过调用 Rails.cache 来访问缓存。
5.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。
5.2. ActiveSupport::Cache::Store
ActiveSupport::Cache::Store 为 Rails 中与缓存交互提供了基础。这是一个抽象类,你不能单独使用它。相反,你必须使用与存储引擎绑定的该类的具体实现。Rails 附带了几个实现,如下所述。
主要的 API 方法是 read、write、delete、exist? 和 fetch。
传递给缓存存储构造函数的选项将被视为相应 API 方法的默认选项。
5.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 控制台手动读取、写入或使缓存过期。
5.4. ActiveSupport::Cache::FileStore
ActiveSupport::Cache::FileStore 使用文件系统存储条目。在初始化缓存时必须指定存储文件的目录路径。
config.cache_store = :file_store, "/path/to/cache/directory"
使用此缓存存储,同一主机上的多个服务器进程可以共享一个缓存。此缓存存储适用于由一两个主机提供服务的低到中等流量站点。在不同主机上运行的服务器进程可以通过使用共享文件系统来共享缓存,但不建议这种设置。
由于缓存会一直增长直到磁盘满,建议定期清除旧条目。
5.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 在 localhost 上以默认端口 (127.0.0.1:11211) 运行,但这对于大型站点来说不是理想的设置。
config.cache_store = :mem_cache_store # Will fallback to $MEMCACHE_SERVERS, then 127.0.0.1:11211
请参阅 Dalli::Client 文档 以了解支持的地址类型。
此缓存上的 write(和 fetch)方法接受利用 memcached 特定功能的附加选项。
5.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,返回给用户的值,通常是 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 }
}
}
5.7. ActiveSupport::Cache::NullStore
ActiveSupport::Cache::NullStore 作用域限制在每个 Web 请求中,并在请求结束时清除存储的值。它旨在用于开发和测试环境。当你有一些代码直接与 Rails.cache 交互但缓存干扰了查看代码更改结果时,它非常有用。
config.cache_store = :null_store
5.8. 自定义缓存存储
你可以通过简单地扩展 ActiveSupport::Cache::Store 并实现适当的方法来创建自己的自定义缓存存储。这样,你就可以将任意数量的缓存技术交换到你的 Rails 应用程序中。
要使用自定义缓存存储,只需将缓存存储设置为自定义类的新实例即可。
config.cache_store = MyCacheStore.new
6. 缓存键
缓存中使用的键可以是任何响应 cache_key 或 to_param 的对象。如果需要生成自定义键,你可以在类上实现 cache_key 方法。Active Record 将根据类名和记录 ID 生成键。
你可以使用哈希和值数组作为缓存键。
# This is a valid cache key
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
你在 Rails.cache 上使用的键与实际用于存储引擎的键不同。它们可能会被命名空间修改或更改以适应技术后端限制。这意味着,例如,你不能使用 Rails.cache 保存值,然后尝试使用 dalli gem 将它们取出。但是,你也不必担心超出 memcached 大小限制或违反语法规则。
7. 条件 GET 支持
条件 GET 是 HTTP 规范的一个特性,它提供了一种方式,让 Web 服务器告诉浏览器对 GET 请求的响应自上次请求以来没有改变,并且可以安全地从浏览器缓存中提取。
它们通过使用 HTTP_IF_NONE_MATCH 和 HTTP_IF_MODIFIED_SINCE 标头来回传递唯一的 content identifier 和内容上次更改的时间戳。如果浏览器发出的请求中 content identifier (ETag) 或上次修改的时间戳与服务器版本匹配,则服务器只需发送回一个不带内容且状态为 “not modified” 的空响应。
服务器(即我们)的责任是查找上次修改的时间戳和 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_at 和 cache_key_with_version 方法来设置 last_modified 和 etag:
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_modified 和 etag 都设置时,行为会根据 config.action_dispatch.strict_freshness 的值而异。如果设置为 true,则仅考虑 etag,如 RFC 7232 第 6 节所述。如果设置为 false,则两者都考虑,并且如果两个条件都满足,则缓存被认为是新鲜的,这是 Rails 的历史行为。
有时我们想缓存响应,例如一个永不失效的静态页面。为此,我们可以使用 http_cache_forever 辅助方法,通过这样做,浏览器和代理将无限期地缓存它。
默认情况下,缓存的响应是私有的,仅缓存在用户的网络浏览器上。要允许代理缓存响应,请设置 public: true 以指示它们可以将缓存的响应提供给所有用户。
使用此辅助方法,last_modified 标头设置为 Time.new(2011, 1, 1).utc,expires 标头设置为 100 年。
谨慎使用此方法,因为浏览器/代理将无法使缓存的响应失效,除非强制清除浏览器缓存。
class HomeController < ApplicationController
def index
http_cache_forever(public: true) do
render
end
end
end
7.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"