更多資訊請參考 rubyonrails.org:

Rails 快取:概觀

本指南簡介如何使用快取來加速您的 Rails 應用程式。

快取是指儲存在請求-回應週期中產生的內容,並在回應類似請求時重複使用。

快取通常是提高應用程式效能最有效的方法。透過快取,在單一伺服器上執行,且具有單一資料庫的網站可以維持數千個並行使用者的負載。

Rails 開箱即用地提供了一組快取功能。本指南將教您每個快取功能的範圍和用途。掌握這些技術,您的 Rails 應用程式可以服務數百萬個視圖,而不會產生過高的回應時間或伺服器費用。

閱讀本指南後,您將了解

  • 片段快取和俄羅斯娃娃快取。
  • 如何管理快取相依性。
  • 替代快取儲存。
  • 條件式 GET 支援。

1 基本快取

這是對三種快取技術的介紹:頁面、動作和片段快取。預設情況下,Rails 提供片段快取。為了使用頁面和動作快取,您需要將 actionpack-page_cachingactionpack-action_caching 新增至您的 Gemfile

預設情況下,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 動作快取

頁面快取不能用於具有 before 篩選器的動作,例如需要身份驗證的頁面。這就是動作快取發揮作用的地方。動作快取的工作方式與頁面快取類似,不同之處在於傳入的 Web 請求會命中 Rails 堆疊,以便在提供快取之前,可以在其上執行 before 篩選器。這允許在仍然提供快取副本的輸出結果時,執行身份驗證和其他限制。

動作快取已從 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 的動作也會為相關聯的產品變更,從而使快取過期。

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 顯式依賴

有時你會遇到無法推導的範本依賴。當渲染發生在 helpers 中時,通常會發生這種情況。以下是一個例子

<%= 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 外部依賴

如果你在快取的區塊中使用 helper 方法,然後更新該 helper,你也必須更新快取。如何更新並不重要,但範本檔案的 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"

使用此快取儲存,同一主機上的多個伺服器處理程序可以共享快取。此快取儲存適用於在一或兩台主機上提供服務的低到中等流量的網站。在不同主機上執行的伺服器處理程序可以使用共享檔案系統來共享快取,但不建議使用該設定。

由於快取將會持續增長直到磁碟滿載,因此建議定期清除舊的項目。

如果未提供明確的 config.cache_store,這是預設的快取儲存實作(位於 "#{root}/tmp/cache/")。

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,即傳回給使用者的值,通常是 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 的範圍限定於每個網路請求,並在請求結束時清除儲存的值。它旨在用於開發和測試環境。當你的程式碼直接與 Rails.cache 互動,但快取會干擾查看程式碼變更的結果時,它非常有用。

config.cache_store = :null_store

2.8 自訂快取儲存

你可以透過簡單地擴展 ActiveSupport::Cache::Store 並實作適當的方法來建立自己的自訂快取儲存。這樣,你可以在 Rails 應用程式中交換任何數量的快取技術。

若要使用自訂快取儲存,只需將快取儲存設定為你的自訂類別的新實例。

config.cache_store = MyCacheStore.new

3 快取鍵

快取中使用的鍵可以是任何回應 cache_keyto_param 的物件。如果需要產生自訂鍵,您可以在類別中實作 cache_key 方法。Active Record 將根據類別名稱和記錄 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 規範的一項功能,它提供了一種讓網路伺服器告知瀏覽器 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 輔助方法,這樣瀏覽器和代理就會無限期地快取它。

預設情況下,快取的回應將是私有的,僅快取在使用者的網路瀏覽器上。要允許代理快取回應,請設定 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

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 快取預設為停用。

要啟用 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 參考資料



回到頂端