v7.1.3.2
更多資訊請至 rubyonrails.org: 更多 Ruby on Rails

Rails 快取:概觀

本指南是使用快取加速 Rails 應用程式的入門指南。

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

快取通常是提升應用程式效能最有效的方法。透過快取,在單一伺服器上執行、搭配單一資料庫的網站,可以負擔數千個同時使用者的負載。

Rails 提供一套現成的快取功能。本指南將教導您每個功能的範圍和目的。掌握這些技巧,您的 Rails 應用程式就能提供數百萬次檢視,而不會有過長的回應時間或伺服器帳單。

閱讀本指南後,您將會了解

1 基本快取

這是三種類型快取技術的簡介:頁面、動作和片段快取。Rails 預設提供片段快取。若要使用頁面和動作快取,您需要將 actionpack-page_cachingactionpack-action_caching 新增到您的 Gemfile

預設情況下,快取僅在您的生產環境中啟用。您可以透過執行 rails dev:cache,或在 config/environments/development.rb 中將 config.action_controller.perform_caching 設定為 true,在本地端使用快取。

變更 config.action_controller.perform_caching 的值,只會對 Action Controller 提供的快取產生影響。例如,它不會影響低階快取,我們會在 下方說明。

1.1 頁面快取

頁面快取是一種 Rails 機制,允許由網頁伺服器(例如 Apache 或 NGINX)滿足已產生頁面的要求,而無需經過整個 Rails 堆疊。雖然這非常快速,但無法套用於每個情況(例如需要驗證的頁面)。此外,由於網頁伺服器直接從檔案系統提供檔案,因此您需要實作快取到期日。

Rails 4 已移除頁面快取。請參閱 actionpack-page_caching gem

1.2 Action 快取

頁面快取無法用於有先執行篩選器的動作,例如需要驗證的頁面。這時就需要 Action 快取。Action 快取的運作方式類似頁面快取,但會先讓進入的網路要求進入 Rails 堆疊,以便在提供快取之前對其執行先執行篩選器。這允許在提供快取副本的輸出結果時,同時執行驗證和其他限制。

Rails 4 已移除 Action 快取。請參閱 actionpack-action_caching gem。請參閱 DHH 的基於金鑰的快取到期概觀,以了解新推薦的方法。

1.3 片段快取

動態網路應用程式通常會使用各種元件來建構頁面,而這些元件不一定都具有相同的快取特性。當頁面的不同部分需要個別快取和到期時,可以使用片段快取。

片段快取允許將檢視邏輯的片段包裝在快取區塊中,並在下次要求進入時從快取儲存中提供。

例如,如果您想要快取頁面上的每個產品,可以使用以下程式碼

<% @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 %>

所有來自前一次呈現的快取範本都會以更快的速度一次擷取。此外,尚未快取的範本會寫入快取,並在下次呈現時進行多重擷取。

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 的檔案。

另一個選項是包含要呈現的部分的完整檔名。

render(partial: 'hotels/hotel.html.erb', collection: @hotels, 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"

使用此快取儲存體,同一個主機上的多個伺服器程序可以共享快取。此快取儲存體適用於從一個或兩個主機提供服務的低流量到中流量網站。執行於不同主機上的伺服器程序可以使用共用檔案系統來共享快取,但建議不要這樣做。

由於快取會持續增加,直到磁碟空間已滿,建議定期清除舊的項目。

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

2.5 ActiveSupport::Cache::MemCacheStore

ActiveSupport::Cache::MemCacheStore 使用 Danga 的 memcached 伺服器,為您的應用程式提供集中式快取。預設情況下,Rails 使用內建的 dalli 寶石。這目前是製作網站最受歡迎的快取儲存。它可用於提供單一、共用的快取叢集,具有極高的效能和備援性。

初始化快取時,您應該指定叢集中所有 memcached 伺服器的位址,或確保 MEMCACHE_SERVERS 環境變數已適當地設定。

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

如果兩者都沒有指定,它將假設 memcached 在預設埠號 (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 特有的功能。

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 寶石新增到您的 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

有時我們想要快取回應,例如靜態頁面,永遠不會過期。為達成此目的,我們可以使用 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 開發中的快取

在開發模式中測試應用程式的快取策略是很常見的。Rails 提供 rails 指令 dev:cache 以輕鬆地開啟/關閉快取。

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

預設情況下,當開發模式快取為關閉時,Rails 會使用 :null_store

6 個參考

回饋

我們鼓勵您協助提升本指南的品質。

如果您發現任何錯字或事實錯誤,請協助我們修正。若要開始,您可以閱讀我們的 文件貢獻 部分。

您也可能會發現不完整或過時的內容。請務必為主程式碼新增任何遺漏的文件。請務必先查看 Edge Guides,以驗證問題是否已在主分支中修正。查看 Ruby on Rails 指南準則 以了解樣式和慣例。

如果您發現需要修正之處,但無法自行修補,請 開啟問題

最後,我們非常歡迎在 官方 Ruby on Rails 論壇 上針對 Ruby on Rails 文件進行任何討論。