# Rails 緩存簡介
本文要教你如果避免頻繁查詢數據庫,在最短的時間內把真正需要的內容返回給客戶端。
讀完本文,你將學到:
* 頁面和動作緩存(在 Rails 4 中被提取成單獨的 gem);
* 片段緩存;
* 存儲緩存的方法;
* Rails 對條件 GET 請求的支持;
### Chapters
1. [緩存基礎](#%E7%BC%93%E5%AD%98%E5%9F%BA%E7%A1%80)
* [頁面緩存](#%E9%A1%B5%E9%9D%A2%E7%BC%93%E5%AD%98)
* [動作緩存](#%E5%8A%A8%E4%BD%9C%E7%BC%93%E5%AD%98)
* [片段緩存](#%E7%89%87%E6%AE%B5%E7%BC%93%E5%AD%98)
* [底層緩存](#%E5%BA%95%E5%B1%82%E7%BC%93%E5%AD%98)
* [SQL 緩存](#sql-%E7%BC%93%E5%AD%98)
2. [緩存的存儲方式](#%E7%BC%93%E5%AD%98%E7%9A%84%E5%AD%98%E5%82%A8%E6%96%B9%E5%BC%8F)
* [設置](#%E8%AE%BE%E7%BD%AE)
* [ActiveSupport::Cache::Store](#activesupport::cache::store)
* [ActiveSupport::Cache::MemoryStore](#activesupport::cache::memorystore)
* [ActiveSupport::Cache::FileStore](#activesupport::cache::filestore)
* [ActiveSupport::Cache::MemCacheStore](#activesupport::cache::memcachestore)
* [ActiveSupport::Cache::EhcacheStore](#activesupport::cache::ehcachestore)
* [ActiveSupport::Cache::NullStore](#activesupport::cache::nullstore)
* [自建存儲方式](#%E8%87%AA%E5%BB%BA%E5%AD%98%E5%82%A8%E6%96%B9%E5%BC%8F)
* [緩存鍵](#%E7%BC%93%E5%AD%98%E9%94%AE)
3. [支持條件 GET 請求](#%E6%94%AF%E6%8C%81%E6%9D%A1%E4%BB%B6-get-%E8%AF%B7%E6%B1%82)
### 1 緩存基礎
本節介紹三種緩存技術:頁面,動作和片段。Rails 默認支持片段緩存。如果想使用頁面緩存和動作緩存,要在 `Gemfile` 中加入 `actionpack-page_caching` 和 `actionpack-action_caching`。
在開發環境中若想使用緩存,要把 `config.action_controller.perform_caching` 選項設為 `true`。這個選項一般都在各環境的設置文件(`config/environments/*.rb`)中設置,在開發環境和測試環境默認是禁用的,在生產環境中默認是開啟的。
```
config.action_controller.perform_caching = true
```
#### 1.1 頁面緩存
頁面緩存機制允許網頁服務器(Apache 或 Nginx 等)直接處理請求,不經 Rails 處理。這么做顯然速度超快,但并不適用于所有情況(例如需要身份認證的頁面)。服務器直接從文件系統上伺服文件,所以緩存過期是一個很棘手的問題。
Rails 4 刪除了對頁面緩存的支持,如想使用就得安裝 [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching)。最新推薦的緩存方法參見 [DHH 對鍵基緩存過期的介紹](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)。
#### 1.2 動作緩存
如果動作上有前置過濾器就不能使用頁面緩存,例如需要身份認證的頁面,這時需要使用動作緩存。動作緩存和頁面緩存的工作方式差不多,但請求還是會經由 Rails 處理,所以在伺服緩存之前會執行前置過濾器。使用動作緩存可以執行身份認證等限制,然后再從緩存中取出結果返回客戶端。
Rails 4 刪除了對動作緩存的支持,如想使用就得安裝 [actionpack-action_caching gem](https://github.com/rails/actionpack-action_caching)。最新推薦的緩存方法參見 [DHH 對鍵基緩存過期的介紹](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)。
#### 1.3 片段緩存
如果能緩存整個頁面或動作的內容,再伺服給客戶端,這個世界就完美了。但是,動態網頁程序的頁面一般都由很多部分組成,使用的緩存機制也不盡相同。在動態生成的頁面中,不同的內容要使用不同的緩存方式和過期日期。為此,Rails 提供了一種緩存機制叫做“片段緩存”。
片段緩存把視圖邏輯的一部分打包放在 `cache` 塊中,后續請求都會從緩存中伺服這部分內容。
例如,如果想實時顯示網站的訂單,而且不想緩存這部分內容,但想緩存顯示所有可選商品的部分,就可以使用下面這段代碼:
```
<% Order.find_recent.each do |o| %>
<%= o.buyer.name %> bought <%= o.product.name %>
<% end %>
<% cache do %>
All available products:
<% Product.all.each do |p| %>
<%= link_to p.name, product_url(p) %>
<% end %>
<% end %>
```
上述代碼中的 `cache` 塊會綁定到調用它的動作上,輸出到動作緩存的所在位置。因此,如果要在動作中使用多個片段緩存,就要使用 `action_suffix` 為 `cache` 塊指定前綴:
```
<% cache(action: 'recent', action_suffix: 'all_products') do %>
All available products:
```
`expire_fragment` 方法可以把緩存設為過期,例如:
```
expire_fragment(controller: 'products', action: 'recent', action_suffix: 'all_products')
```
如果不想把緩存綁定到調用它的動作上,調用 `cahce` 方法時可以使用全局片段名:
```
<% cache('all_available_products') do %>
All available products:
<% end %>
```
在 `ProductsController` 的所有動作中都可以使用片段名調用這個片段緩存,而且過期的設置方式不變:
```
expire_fragment('all_available_products')
```
如果不想手動設置片段緩存過期,而想每次更新商品后自動過期,可以定義一個幫助方法:
```
module ProductsHelper
def cache_key_for_products
count = Product.count
max_updated_at = Product.maximum(:updated_at).try(:utc).try(:to_s, :number)
"products/all-#{count}-#{max_updated_at}"
end
end
```
這個方法生成一個緩存鍵,用于所有商品的緩存。在視圖中可以這么做:
```
<% cache(cache_key_for_products) do %>
All available products:
<% end %>
```
如果想在滿足某個條件時緩存片段,可以使用 `cache_if` 或 `cache_unless` 方法:
```
<% cache_if (condition, cache_key_for_products) do %>
All available products:
<% end %>
```
緩存的鍵名還可使用 Active Record 模型:
```
<% Product.all.each do |p| %>
<% cache(p) do %>
<%= link_to p.name, product_url(p) %>
<% end %>
<% end %>
```
Rails 會在模型上調用 `cache_key` 方法,返回一個字符串,例如 `products/23-20130109142513`。鍵名中包含模型名,ID 以及 `updated_at` 字段的時間戳。所以更新商品后會自動生成一個新片段緩存,因為鍵名變了。
上述兩種緩存機制還可以結合在一起使用,這叫做“俄羅斯套娃緩存”(Russian Doll Caching):
```
<% cache(cache_key_for_products) do %>
All available products:
<% Product.all.each do |p| %>
<% cache(p) do %>
<%= link_to p.name, product_url(p) %>
<% end %>
<% end %>
<% end %>
```
之所以叫“俄羅斯套娃緩存”,是因為嵌套了多個片段緩存。這種緩存的優點是,更新單個商品后,重新生成外層片段緩存時可以繼續使用內層片段緩存。
#### 1.4 底層緩存
有時不想緩存視圖片段,只想緩存特定的值或者查詢結果。Rails 中的緩存機制可以存儲各種信息。
實現底層緩存最有效地方式是使用 `Rails.cache.fetch` 方法。這個方法既可以從緩存中讀取數據,也可以把數據寫入緩存。傳入單個參數時,讀取指定鍵對應的值。傳入代碼塊時,會把代碼塊的計算結果存入緩存的指定鍵中,然后返回計算結果。
以下面的代碼為例。程序中有個 `Product` 模型,其中定義了一個實例方法,用來查詢競爭對手網站上的商品價格。這個方法的返回結果最好使用底層緩存:
```
class Product < ActiveRecord::Base
def competing_price
Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
Competitor::API.find_price(id)
end
end
end
```
注意,在這個例子中使用了 `cache_key` 方法,所以得到的緩存鍵名是這種形式:`products/233-20140225082222765838000/competing_price`。`cache_key` 方法根據模型的 `id` 和 `updated_at` 屬性生成鍵名。這是最常見的做法,因為商品更新后,緩存就失效了。一般情況下,使用底層緩存保存實例的相關信息時,都要生成緩存鍵。
#### 1.5 SQL 緩存
查詢緩存是 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 為動作緩存和片段緩存提供了不同的存儲方式。
頁面緩存全部存儲在硬盤中。
#### 2.1 設置
程序默認使用的緩存存儲方式可以在文件 `config/application.rb` 的 `Application` 類中或者環境設置文件(`config/environments/*.rb`)的 `Application.configure` 代碼塊中調用 `config.cache_store=` 方法設置。該方法的第一個參數是存儲方式,后續參數都是傳給對應存儲方式構造器的參數。
```
config.cache_store = :memory_store
```
在設置代碼塊外部可以調用 `ActionController::Base.cache_store` 方法設置存儲方式。
緩存中的數據通過 `Rails.cache` 方法獲取。
#### 2.2 ActiveSupport::Cache::Store
這個類提供了在 Rails 中和緩存交互的基本方法。這是個抽象類,不能直接使用,應該使用針對各存儲引擎的具體實現。Rails 實現了幾種存儲方式,介紹參見后幾節。
和緩存交互常用的方法有:`read`,`write`,`delete`,`exist?`,`fetch`。`fetch` 方法接受一個代碼塊,如果緩存中有對應的數據,將其返回;否則,執行代碼塊,把結果寫入緩存。
Rails 實現的所有存儲方式都共用了下面幾個選項。這些選項可以傳給構造器,也可傳給不同的方法,和緩存中的記錄交互。
* `:namespace`:在緩存存儲中創建命名空間。如果和其他程序共用同一個存儲,可以使用這個選項。
* `:compress`:是否壓縮緩存。便于在低速網絡中傳輸大型緩存記錄。
* `:compress_threshold`:結合 `:compress` 選項使用,設定一個閾值,低于這個值就不壓縮緩存。默認為 16 KB。
* `:expires_in`:為緩存記錄設定一個過期時間,單位為秒,過期后把記錄從緩存中刪除。
* `:race_condition_ttl`:結合 `:expires_in` 選項使用。緩存過期后,禁止多個進程同時重新生成同一個緩存記錄(叫做 dog pile effect),從而避免條件競爭。這個選項設置一個秒數,在這個時間之后才能再次使用重新生成的新值。如果設置了 `:expires_in` 選項,最好也設置這個選項。
#### 2.3 ActiveSupport::Cache::MemoryStore
這種存儲方式在 Ruby 進程中把緩存保存在內存中。存儲空間的大小由 `:size` 選項指定,默認為 32MB。如果超出分配的大小,系統會清理緩存,把最不常使用的記錄刪除。
```
config.cache_store = :memory_store, { size: 64.megabytes }
```
如果運行多個 Rails 服務器進程(使用 mongrel_cluster 或 Phusion Passenger 時),進程間無法共用緩存數據。這種存儲方式不適合在大型程序中使用,不過很適合只有幾個服務器進程的小型、低流量網站,也可在開發環境和測試環境中使用。
#### 2.4 ActiveSupport::Cache::FileStore
這種存儲方式使用文件系統保存緩存。緩存文件的存儲位置必須在初始化時指定。
```
config.cache_store = :file_store, "/path/to/cache/directory"
```
使用這種存儲方式,同一主機上的服務器進程之間可以共用緩存。運行在不同主機上的服務器進程之間也可以通過共享的文件系統共用緩存,但這種用法不是最好的方式,因此不推薦使用。這種存儲方式適合在只用了一到兩臺主機的中低流量網站中使用。
注意,如果不定期清理,緩存會不斷增多,最終會用完硬盤空間。
這是默認使用的緩存存儲方式。
#### 2.5 ActiveSupport::Cache::MemCacheStore
這種存儲方式使用 Danga 開發的 `memcached` 服務器,為程序提供一個中心化的緩存存儲。Rails 默認使用附帶安裝的 `dalli` gem 實現這種存儲方式。這是目前在生產環境中使用最廣泛的緩存存儲方式,可以提供單個緩存存儲,或者共享的緩存集群,性能高,冗余度低。
初始化時要指定集群中所有 memcached 服務器的地址。如果沒有指定地址,默認運行在本地主機的默認端口上,這對大型網站來說不是個好主意。
在這種緩存存儲中使用 `write` 和 `fetch` 方法還可指定兩個額外的選項,充分利用 memcached 的特有功能。指定 `:raw` 選項可以直接把沒有序列化的數據傳給 memcached 服務器。在這種類型的數據上可以使用 memcached 的原生操作,例如 `increment` 和 `decrement`。如果不想讓 memcached 覆蓋已經存在的記錄,可以指定 `:unless_exist` 選項。
```
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
```
#### 2.6 ActiveSupport::Cache::EhcacheStore
如果在 JRuby 平臺上運行程序,可以使用 Terracotta 開發的 Ehcache 存儲緩存。Ehcache 是使用 Java 開發的開源緩存存儲,同時也提供企業版,增強了穩定性、操作便利性,以及商用支持。使用這種存儲方式要先安裝 `jruby-ehcache-rails3` gem(1.1.0 及以上版本)。
```
config.cache_store = :ehcache_store
```
初始化時,可以使用 `:ehcache_config` 選項指定 Ehcache 設置文件的位置(默認為 Rails 程序根目錄中的 `ehcache.xml`),還可使用 `:cache_name` 選項定制緩存名(默認為 `rails_cache`)。
使用 `write` 方法時,除了可以使用通用的 `:expires_in` 選項之外,還可指定 `:unless_exist` 選項,讓 Ehcache 使用 `putIfAbsent` 方法代替 `put` 方法,不覆蓋已經存在的記錄。除此之外,`write` 方法還可接受 [Ehcache Element 類](http://ehcache.org/apidocs/net/sf/ehcache/Element.html)開放的所有屬性,包括:
| 屬性 | 參數類型 | 說明 |
| --- | --- | --- |
| elementEvictionData | ElementEvictionData | 設置元素的 eviction 數據實例 |
| eternal | boolean | 設置元素是否為 eternal |
| timeToIdle, tti | int | 設置空閑時間 |
| timeToLive, ttl, expires_in | int | 設置在線時間 |
| version | long | 設置 ElementAttributes 對象的 `version` 屬性 |
這些選項通過 Hash 傳給 `write` 方法,可以使用駝峰式或者下劃線分隔形式。例如:
```
Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds)
caches_action :index, expires_in: 60.seconds, unless_exist: true
```
關于 Ehcache 更多的介紹,請訪問 [http://ehcache.org/](http://ehcache.org/)。關于如何在運行于 JRuby 平臺之上的 Rails 中使用 Ehcache,請訪問 [http://ehcache.org/documentation/jruby.html](http://ehcache.org/documentation/jruby.html)。
#### 2.7 ActiveSupport::Cache::NullStore
這種存儲方式只可在開發環境和測試環境中使用,并不會存儲任何數據。如果在開發過程中必須和 `Rails.cache` 交互,而且會影響到修改代碼后的效果,使用這種存儲方式尤其方便。使用這種存儲方式時調用 `fetch` 和 `read` 方法沒有實際作用。
```
config.cache_store = :null_store
```
#### 2.8 自建存儲方式
要想自建緩存存儲方式,可以繼承 `ActiveSupport::Cache::Store` 類,并實現相應的方法。自建存儲方式時,可以使用任何緩存技術。
使用自建的存儲方式,把 `cache_store` 設為類的新實例即可。
```
config.cache_store = MyCacheStore.new
```
#### 2.9 緩存鍵
緩存中使用的鍵可以是任意對象,只要能響應 `:cache_key` 或 `:to_param` 方法即可。如果想生成自定義鍵,可以在類中定義 `:cache_key` 方法。Active Record 根據類名和記錄的 ID 生成緩存鍵。
緩存鍵也可使用 Hash 或者數組。
```
# This is a legal cache key
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
```
`Rails.cache` 方法中使用的鍵和保存到存儲引擎中的鍵并不一樣。保存時,可能會根據命名空間或引擎的限制做修改。也就是說,不能使用 `memcache-client` gem 調用 `Rails.cache` 方法保存緩存再嘗試讀取緩存。不過,無需擔心會超出 memcached 的大小限制,或者違反句法規則。
### 3 支持條件 GET 請求
條件請求是 HTTP 規范的一個特性,網頁服務器告訴瀏覽器 GET 請求的響應自上次請求以來沒有發生變化,可以直接讀取瀏覽器緩存中的副本。
條件請求通過 `If-None-Match` 和 `If-Modified-Since` 報頭實現,這兩個報頭的值分別是內容的唯一 ID 和上次修改內容的時間戳,在服務器和客戶端之間來回傳送。如果瀏覽器發送的請求中內容 ID(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)
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
```
如果不想使用 Hash,還可直接傳入模型實例,Rails 會調用 `updated_at` 和 `cache_key` 方法分別設置 `last_modified` 和 `etag`:
```
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
respond_with(@product) if stale?(@product)
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://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes