1 基本快取
這是三種類型快取技術的簡介:頁面、動作和片段快取。Rails 預設提供片段快取。若要使用頁面和動作快取,您需要將 actionpack-page_caching
和 actionpack-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_if
或 cache_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_price
。cache_key_with_version
會根據模型的類別名稱、id
和 updated_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 方法是 read
、write
、delete
、exist?
和 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_key
或 to_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_MATCH
和 HTTP_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_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
有時我們想要快取回應,例如靜態頁面,永遠不會過期。為達成此目的,我們可以使用 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 文件進行任何討論。