1 基本快取
這是對三種快取技術的介紹:頁面、action 和
片段快取。預設情況下,Rails 提供片段快取。為了使用
頁面和 action 快取,您需要新增 actionpack-page_caching
和
actionpack-action_caching
到您的 Gemfile
。
預設情況下,快取僅在您的生產環境中啟用。你可以玩
透過執行 rails dev:cache
或透過設定在本地快取
config.action_controller.perform_caching
到 config/environments/development.rb
中的 true
。
改變 config.action_controller.perform_caching
的 value 將
只對 Action Controller 提供的快取有影響。
例如,它不會影響我們解決的低階快取
下面。
1.1 頁面快取
頁面快取是一種 Rails 機制,它允許請求生成的頁面 由 Web 伺服器(即 Apache 或 NGINX)完成而無需去 透過整個 Rails 堆疊。雖然這是超快的,但它不能應用於 每種情況(例如需要身份驗證的頁面)。另外,由於 Web 伺服器直接從您需要的檔案系統提供檔案 實現快取過期。
資訊:頁面快取已從 Rails 4 中刪除。請參閱 actionpack-page_caching gem。
1.2 Action 快取
頁面快取不能用於具有前置過濾器的操作 - 例如,需要身份驗證的頁面。這就是 Action 快取的用武之地。 Action 快取的工作方式類似於頁面快取,不同之處在於傳入的 Web 請求會命中 Rails 堆疊,以便在提供快取之前可以在其上執行過濾器。這允許執行身份驗證和其他限制,同時仍然提供快取副本的輸出結果。
資訊:Action 快取已從 Rails 4 中刪除。請參閱 actionpack-action_caching gem。有關新首選的方法,請參閱 DHH 根據 key 的快取過期超過 view。
1.3 片段快取
動態 Web 應用程式通常構建具有各種元件的頁面,而不是 所有這些都具有相同的快取特性。當不同部位 頁面需要單獨快取和過期,您可以使用片段快取。
片段快取允許將 view 邏輯的片段包裝在快取塊中,並在下一個請求進入時從快取儲存中提供服務。
例如,如果你想快取頁面上的每個產品,你可以使用這個 程式碼:
<% @products.each do |product| %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
當您的應用程式收到它對該頁面的第一個請求時,Rails 將編寫 具有唯一 key 的新快取條目。 key 看起來像這樣:
views/products/index:bea67108094918eeba42cd4a6e786901/products/1
中間的字串是模板樹摘要。這是一個雜湊 根據您正在快取的 view 片段的內容計算的摘要。如果 您更改 view 片段(例如,HTML 更改),摘要將更改, 使現有檔案過期。
從產品記錄派生的快取版本儲存在快取條目中。 當產品被觸控時,快取版本發生變化,任何快取的片段 包含以前版本的將被忽略。
提示:像 Memcached 這樣的快取儲存會自動刪除舊的快取檔案。
如果要在某些條件下快取片段,可以使用
cache_if
或 cache_unless
:
<% cache_if admin?, product do %>
<%= render product %>
<% end %>
1.3.1 集合快取
render
helper 還可以快取為集合呈現的各個模板。
它甚至可以透過讀取所有快取來使用 each
來完成前面的示例
模板一次而不是一個接一個。這是透過在渲染集合時傳遞 cached: true
來完成的:
<%= render partial: 'products/product', collection: @products, cached: true %>
以前渲染中的所有快取模板將被一次獲取 速度更快。此外,尚未快取的模板將是 寫入快取並在下一次渲染時多次提取。
1.4 俄羅斯娃娃快取
您可能希望將快取片段巢狀在其他快取片段中。這是 稱為俄羅斯娃娃快取。
俄羅斯娃娃快取的優勢在於,如果單個產品更新, 所有其他內部碎片在再生外部碎片時都可以重複使用 分段。
如上一節所述,如果 value 的 value
updated_at
更改為快取檔案直接依賴的記錄。
但是,這不會使片段巢狀在其中的任何快取過期。
以下面的view為例:
<% cache product do %>
<%= render product.games %>
<% end %>
這反過來呈現這個 view:
<% cache game do %>
<%= render game %>
<% end %>
如果更改遊戲的任何屬性,則 updated_at
value 將設定為
當前時間,從而使快取過期。但是,因為 updated_at
不會為產品物件更改,該快取不會過期,並且
您的應用程式將提供陳舊資料。為了解決這個問題,我們將 models 與
touch
方法:
class Product < ApplicationRecord
has_many :games
end
class Game < ApplicationRecord
belongs_to :product, touch: true
end
將 touch
設定為 true
時,任何 action 會更改遊戲的 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 足夠聰明,可以處理常見的情況,所以你不會 必須指定任何東西。但是,有時,當您處理自定義 例如 helpers,您需要明確定義它們。
1.6.1 隱式依賴
大多數模板依賴都可以從模板中對 render
的呼叫中匯出
本身。以下是一些 ActionView::Digestor
知道的渲染呼叫示例
如何解碼:
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 低階快取
有時您需要快取特定的 value 或查詢結果,而不是快取 view 片段。 Rails 的快取機制非常適合儲存 any 型別的資訊。
實現低階快取的最有效方法是使用 Rails.cache.fetch
方法。此方法同時讀取和寫入快取。當只傳遞一個引數時,從快取中獲取 key 並返回 value。如果傳遞了一個塊,則該塊將在快取記憶體未命中時執行。塊的返回 value 將寫入給定快取 key 下的快取,返回 value 將被返回。在快取命中的情況下,快取的 value 將在不執行塊的情況下返回。
考慮以下示例。一個應用程式有一個 Product
model 和一個在競爭網站上查詢產品價格的實例方法。此方法返回的資料非常適合低階快取:
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
方法,因此生成的快取 key 將類似於 products/233-20140225082222765838000/competing_price
。 cache_key_with_version
根據 model 的類名、id
和 updated_at
屬性生成一個字串。這是一個常見的約定,並且具有在產品更新時使快取失效的好處。一般來說,當你對實例級別的資訊使用低階快取時,你需要生成一個快取key。
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
第二次對資料庫執行相同的查詢時,它實際上不會命中資料庫。第一次從查詢返回結果時,它儲存在查詢快取(記憶體中)中,第二次從記憶體中提取。
但是,重要的是要注意查詢快取是在開始時建立的 一個 action 並在該 action 結束時銷燬,因此僅在 action 的持續時間。如果您想將查詢結果儲存在更多 持久時尚,您可以使用低階快取。
2 快取儲存
Rails 為快取資料提供了不同的儲存(除了 SQL 和頁面 快取)。
2.1 設定
您可以透過設定應用程式的預設快取儲存
config.cache_store
設定選項。其他引數可以作為
快取儲存建構函式的引數:
config.cache_store = :memory_store, { size: 64.megabytes }
或者,您可以在設定塊之外呼叫 ActionController::Base.cache_store
。
您可以透過呼叫 Rails.cache
來訪問快取。
2.2 ActiveSupport::Cache::Store
此類為與 Rails 中的快取互動提供了基礎。這是一個抽象類,您不能單獨使用它。相反,您必須使用繫結到儲存引擎的類的具體實現。 Rails 附帶了以下記錄的幾個實現。
呼叫的主要方法是read
、write
、delete
、exist?
和fetch
。 fetch 方法獲取一個塊並從快取中返回一個現有的 value,或者如果不存在 value 則評估該塊並將結果寫入快取。
有一些通用選項可供所有快取實現使用。這些可以傳遞給建構函式或與條目互動的各種方法。
:namespace
- 此選項可用於在快取儲存中建立名稱空間。如果您的應用程式與其他應用程式共享快取,這將特別有用。:compress
- 預設啟用。壓縮快取條目,以便在相同的記憶體佔用空間中儲存更多資料,從而減少快取逐出並提高命中率。:compress_threshold
- 預設為 1kB。大於此閾值(以位元組為單位)的快取條目將被壓縮。:expires_in
- 此選項設定快取條目的過期時間(以秒為單位),如果快取儲存支援它,它將自動從快取中刪除。:race_condition_ttl
- 此選項與:expires_in
選項結合使用。它將透過防止多個程序同時重新生成相同的條目(也稱為狗堆效應)來防止快取條目到期時的競爭條件。此選項設定在重新生成新的 value 時可以重新使用過期條目的秒數。如果您使用:expires_in
選項,最好設定此 value。:coder
- 此選項允許用自定義的替換預設快取條目序列化機制。coder
必須回應dump
和load
,並透過自定義編碼器禁用自動壓縮。
2.2.1 連線池選項
預設情況下,MemCacheStore
和 RedisCacheStore
使用單個連線
每個程序。這意味著如果您使用 Puma 或其他執行緒伺服器,
您可以有多個執行緒等待連線可用。
要增加可用連線的數量,您可以啟用連線
彙集。
首先,將 connection_pool
gem 新增到您的 Gemfile 中:
gem 'connection_pool'
接下來,在設定快取儲存時傳遞 :pool_size
和/或 :pool_timeout
選項:
config.cache_store = :mem_cache_store, "cache.example.com", { pool_size: 5, pool_timeout: 5 }
:pool_size
- 此選項設定每個程序的連線數(預設為 5)。:pool_timeout
- 此選項設定等待連線的秒數(預設為 5)。如果超時內沒有可用連線,則會引發Timeout::Error
。
2.2.2 自定義快取儲存
您可以透過簡單地擴充套件來建立自己的自定義快取儲存
ActiveSupport::Cache::Store
並實現適當的方法。這條路,
您可以將任意數量的快取技術交換到您的 Rails 應用程式中。
要使用自定義快取儲存,只需將快取儲存設定為您的新實例 自定義類。
config.cache_store = MyCacheStore.new
2.3 ActiveSupport::Cache::MemoryStore
此快取儲存將條目儲存在同一 Ruby 程序中的記憶體中。快取
透過將 :size
選項傳送到
初始值設定項(預設為 32Mb)。當快取超過分配的大小時,
將進行清理並刪除最近最少使用的條目。
config.cache_store = :memory_store, { size: 64.megabytes }
如果您在 Rails 伺服器程序上執行多個 Ruby(就是這種情況 如果您使用的是 Phusion Passenger 或 puma 叢集模式),那麼您的 Rails 伺服器 流程實例將無法相互共享快取資料。這個快取 store 不適合大型應用程式部署。然而,它可以 適用於只有幾個伺服器程序的小型、低流量站點, 以及開發和測試環境。
預設情況下,新的 Rails 專案設定為在開發環境中使用此實現。
由於程序在使用 :memory_store
時不會共享快取資料,
無法透過 Rails 控制檯手動讀取、寫入或過期快取。
2.4 ActiveSupport::Cache::FileStore
此快取儲存使用檔案系統來儲存條目。初始化快取時必須指定儲存檔案的目錄路徑。
config.cache_store = :file_store, "/path/to/cache/directory"
使用這個快取儲存,同一主機上的多個伺服器程序可以共享一個 快取。此快取儲存適用於具有以下特點的中低流量站點 服務一兩個主機。執行在不同主機上的伺服器程序可以 使用共享檔案系統共享快取,但不推薦這種設定。
由於快取會一直增長到磁碟已滿,因此建議 定期清除舊條目。
這是預設的快取儲存實現(在 "#{root}/tmp/cache/"
),如果
沒有提供明確的 config.cache_store
。
2.5 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
) 上執行,但這不是大型站點的理想設定。
config.cache_store = :mem_cache_store # Will fallback to $MEMCACHE_SERVERS, then 127.0.0.1:11211
有關支援的地址型別,請參閱 Dalli::Client
文件。
此快取上的 write
和 fetch
方法接受兩個附加選項,這些選項利用特定於 memcached 的功能。您可以指定 :raw
將值直接傳送到伺服器,無需序列化。該值必須是字串或數字。您只能在原始 values 上使用記憶體快取直接操作,如 increment
和 decrement
。如果您不希望 memcached 覆蓋現有條目,您還可以指定 :unless_exist
。
2.6 ActiveSupport::Cache::RedisCacheStore
Redis 快取儲存利用 Redis 支援自動驅逐 當它達到最大記憶體時,讓它的行為很像 Memcached 快取伺服器。
部署 note: Redis 預設不會過期 keys,所以請注意使用 專用的Redis快取伺服器。不要用你的持久 Redis 伺服器填滿 不穩定的快取資料!閱讀 Redis快取伺服器設定指南詳解。
對於僅快取的 Redis 伺服器,將 maxmemory-policy
設定為 allkeys 的變體之一。
Redis 4+ 支援最不常用的驅逐(allkeys-lfu
),一個很好的
預設選擇。 Redis 3 及更早版本應使用最近最少使用的驅逐 (allkeys-lru
)。
將快取讀取和寫入超時設定得相對較低。重新生成快取的 value 通常比等待超過一秒鐘來檢索它更快。既讀又 寫入超時預設為 1 秒,但如果您的網路是 持續低延遲。
預設情況下,快取儲存不會嘗試重新連線到 Redis,如果 請求期間連線失敗。如果您經常斷開連線 可能希望啟用重新連線嘗試。
快取讀取和寫入永遠不會引發異常;他們只是返回 nil
,
表現得好像快取中什麼都沒有。判斷你的快取是否是
遇到異常,您可以提供 error_handler
報告給
異常收集服務。它必須接受三個 keyword 引數:method
,
最初呼叫的快取儲存方法; returning
、value
已返回給使用者,通常為 nil
;和 exception
,例外的是
被救出。
首先,將 redis gem 新增到您的 Gemfile 中:
gem 'redis'
您可以啟用對更快 hiredis 的支援 透過將其 ruby 包裝器額外新增到您的 Gemfile 中來連線庫:
gem 'hiredis'
如果可用,Redis 快取儲存將自動要求並使用hiredis。沒有進一步的 需要設定。
最後在相關的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 20 seconds
read_timeout: 0.2, # Defaults to 1 second
write_timeout: 0.2, # Defaults to 1 second
reconnect_attempts: 1, # Defaults to 0
error_handler: -> (method:, returning:, exception:) {
# Report errors to Sentry as warnings
Raven.capture_exception exception, level: 'warning',
tags: { method: method, returning: returning }
}
}
2.7 ActiveSupport::Cache::NullStore
此快取儲存範圍限定於每個 Web 請求,並在請求結束時清除儲存的 values。它的目標在於用於開發和測試環境。當您的程式碼直接與 Rails.cache
互動但快取會干擾檢視程式碼更改的結果時,它會非常有用。
config.cache_store = :null_store
3 快取 Keys
快取中使用的 keys 可以是回應 cache_key
或
to_param
。如果需要,您可以在類上實現 cache_key
方法
生成自定義 keys。 Active Record 會根據類名生成 keys
並記錄ID。
您可以使用 values 的雜湊和陣列作為快取 keys。
# 這是合法的快取key
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
您在 Rails.cache
上使用的 keys 將與實際使用的不同
儲存引擎。它們可以使用名稱空間進行修改或更改以適應
技術後端限制。這意味著,例如,您無法儲存
values 和 Rails.cache
然後嘗試用 dalli
寶石把它們拉出來。
但是,您也不必擔心超出 memcached 大小限制或
違反語法規則。
4 有條件的 GET 支援
條件 GET 是 HTTP 規範的一個特性,它為 Web 伺服器提供了一種方法來告訴瀏覽器對 GET 請求的回應自上次請求以來沒有改變,並且可以安全地從瀏覽器快取中提取。
它們透過使用 HTTP_IF_NONE_MATCH
和 HTTP_IF_MODIFIED_SINCE
標頭來回傳遞唯一的內容識別符號和內容上次更改時間的時間戳來工作。如果瀏覽器發出請求,其中內容識別符號 (ETag) 或自時間戳記以來的最後修改時間與伺服器的版本匹配,則伺服器只需要發回一個未修改狀態的空回應。
伺服器(即我們)負責查詢最後修改的時間戳和 if-none-match 標頭並確定是否發回完整回應。有了 Rails 中的條件獲取支援,這是一項非常簡單的任務:
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
除了選項雜湊,您還可以簡單地傳入 model。 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
或自己呼叫渲染),那麼您在 fresh_when
中有一個簡單的 helper:
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
helper 並透過這樣做
所以瀏覽器和代理將無限期地快取它。
預設情況下快取的回應將是私有的,僅快取在使用者的網路上
瀏覽器。要允許代理快取回應,請設定 public: true
以指示
他們可以為所有使用者提供快取的回應。
使用這個 helper,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 強v/s 弱ETag
Rails 預設生成弱 ETag。弱 ETag 允許語義等價 回應具有相同的 ETag,即使它們的身體不完全匹配。 當我們不希望頁面因細微更改而重新生成時,這很有用 回應體。
弱 ETag 有一個領先的 W/
以將它們與強 ETag 區分開來。
W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag
"618bbc92e2d35ea1945008b42799b0e7" → Strong ETag
與弱 ETag 不同,強 ETag 意味著回應應該完全相同 和逐位元組相同。在一個範圍內執行 Range 請求時很有用 大影片或 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.
預設情況下,當開發模式快取為 off 時,Rails 使用
ActiveSupport::Cache::NullStore
。
6 參考
回饋
我們鼓勵您幫助提高本指南的品質。
如果您發現任何拼寫錯誤或資訊錯誤,請提供回饋。 要開始回饋,您可以閱讀我們的 回饋 部分。
您還可能會發現不完整的內容或不是最新的內容。 請務必為 main 新增任何遺漏的文件。假設是 非穩定版指南(edge guides) 請先驗證問題是否已經在主分支上解決。 請前往 Ruby on Rails 指南寫作準則 查看寫作風格和慣例。
如果由於某種原因您發現要修復的內容但無法自行修補,請您 提出 issue。
關於 Ruby on Rails 的任何類型的討論歡迎提供任何文件至 rubyonrails-docs 討論區。