# 6.2 緩存
## 概要:
本課時講解 Rails 中如何使用緩存。
## 知識點:
1. 緩存
1. redis
1. memcached
## 正文
### 6.2.1 Rails 緩存
Rails 提供了三種方式的緩存,頁面緩存,方法緩存和片段緩存,在 Rails 4 之前的版本里,它包含在 Rails 中,但是從 4.x 開始,三種緩存中的兩種轉為 gem 形式,只有片段緩存保留在 Rails 默認中。
在開發環境下,緩存是關閉的,如果要測試它,需要更改配置:
~~~
config.action_controller.perform_caching = true
~~~
在產品環境下,它默認是 true。
### 6.2.2 頁面緩存,Page Cache
Rails 4.x 將頁面緩存轉為 [gem](https://github.com/rails/actionpack-page_caching),使用的時候需要加入到 gemfile 中。
我們設置一下緩存路徑,在 `config/environments/development.rb`
~~~
config.action_controller.page_cache_directory = "#{Rails.root.to_s}/public"
~~~
頁面緩存是將整個頁面,生成一份靜態的 html 頁面,這個頁面會保存在剛才設置的目錄中。Rails 在顯示該地址的時候,會優先查找 public 是否有同名的 html 文件優先顯示。
我們把 `show` 方法加入到頁面緩存中:
~~~
class ProductsController < ApplicationController
...
caches_page :show
~~~
當第一次訪問時,會創建該緩存文件:
~~~
Write page /path/to/project/public/products/3.html (9.5ms)
~~~
再次訪問時,便直接讀取該文件,而不再執行 `show` 方法了。
這樣做的好處是,可以把一些經常訪問的頁面作為頁面緩存。缺點是,這種頁面不能有太多用戶的個人信息,因為這個頁面對所有人訪問都是相同的內容。如果必須考慮個人信息,可以改為 js 形式,或者使用方法緩存(Action Cache)。
當這個緩存頁面內容更改時,可以刪掉該文件,再次訪問時會自動創建。也可以在 `update` 內加入過期的命令:
~~~
def update
respond_to do |format|
if @product.update(product_params)
expire_page action: 'show', id: @product.id
...
else
...
end
end
end
~~~
更新資料后會自動過期該文件。
~~~
Expire page /path/to/project/public/products/3.html (1.0ms)
~~~
### 6.2.3 方法緩存,Action Cache
方法緩存和頁面緩存的區別是:它會執行對應的 action 中的代碼。頁面緩存直接讀取緩存文件,不執行 action 中的代碼。
頁面緩存的 [gem](https://github.com/rails/actionpack-action_caching) 在這里。
我們給方法增加方法緩存:
~~~
class ProductsController < ApplicationController
...
caches_action :index, layout: false
~~~
訪問該頁面,會創建一個片段緩存(fragment cache)文件:
~~~
Write fragment views/localhost:3000/products (5.9ms)
~~~
該片段緩存為當前整個頁面,我們增加 `layout: false` 參數,這樣,片段緩存只包含該 action 對應的模板內容,而不包含 layout。我們設計的代碼,將用戶信息放置在 layout 中,登錄后會顯示用戶名。所以 layout 是不應該放到緩存中的。
但是,因為我們給 index 方法增加了搜索功能,而該方法已經加入到了緩存中,所以,搜索是還是顯示的緩存內容。這里可以做調整,要么將搜索放到專用的非緩存方法中,要么搜索時過時該緩存。
### 6.2.4 片段緩存,Fragment Cache
片段緩存,是 Rails 默認使用的緩存方式,它指的是視圖(view)中,緩存局部內容:
~~~
<% cache do %>
分類:
<% Catalog.all.each do |catalog| %>
<%= link_to catalog.name, catalog %>
<% end %>
<% end %>
~~~
這里把經常訪問的分類列表,加入到了緩存中,避免每次頁面訪問該部分都讀取數據庫。
我們可以給 cache 方法增加一些參數:
~~~
<% cache(action: 'new', action_suffix: 'all_products') do %>
~~~
它產生的緩存 key 是:
~~~
Write fragment views/localhost:3000/products/new?action_suffix=all_products/02c540e3ab26f72d5e9273d5824c204e (60.0ms)
~~~
也可以直接命名緩存 key:
~~~
<% cache( "all_products" ) do %>
~~~
它產生的緩存 key 是:
~~~
Write fragment views/all_products/cc926a692262d0e538f07d5dd5d54942 (15.1ms)
~~~
或者直接緩存一個實例:
~~~
<% cache @product do %>
~~~
它產生的緩存 key 是:
~~~
Write fragment views/products/3-20150620164035711340000/b0699b1b8be94ebd1bfcfe74a21571f8 (21.5ms)
~~~
可見,緩存是產生一個 `key: value` 結構的數據。`key` 來自于實例的 `cache_key` 方法:
~~~
p = Product.last
p.cache_key
=> "products/3-20150620164035711340000"
p.updated_at = nil
p.cache_key
=> "products/3"
~~~
該方法會讀取 updated_at 字段值,這樣,每當該實例更改的時候,會自動更新 updated_at 字段,相當于自動更新了緩存。
我們可以使用
~~~
expire_fragment(action: 'new', action_suffix: 'all_products')
expire_fragment("all_products")
~~~
過期這些片段緩存
### 6.2.5 緩存服務
緩存產生的是 `key: value` 結構的數據,所以我們可以使用支持該解構的數據庫來保存緩存。在 `config/environments/production.rb` 中有 cache_store 的選項:
~~~
# Use a different cache store in production.
config.cache_store = :mem_cache_store
~~~
這里有四個選項可以使用::memory_store, :file_store, :mem_cache_store, :null_store。在 [手冊](http://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-ehcachestore)里還介紹了 JRuby 的 Ehcache。
#### 6.2.5.1 :memory_store
緩存和 Ruby 進程使用共同的內存,默認大小為32M,如果超出這個范圍,會移除掉舊的記錄。我們可以更改這個限制:
~~~
config.cache_store = :memory_store, { size: 64.megabytes }
~~~
但是多個 Rails 應用不會共享該緩存。它不適合大型的部署,適合小型的,低訪問量的應用。
#### 6.2.5.2 :file_store
~~~
config.cache_store = :file_store, "/path/to/cache/directory"
~~~
緩存利用文件系統來存放緩存文件,雖然可以在多個應用間共享緩存,但是不建議在產品環境下使用。這種方式會不斷的增加硬盤使用,直到手動清空所有緩存。
Rails 默認使用這種方式。
#### 6.2.5.3 :mem_cache_store
這種方式使用 [Memcached](http://memcached.org/) 最為后端緩存服務,它提供了高性能的、集中式的緩存服務,可以在多個應用間共享緩存,這是一種適合中大型商業應用的選擇。
~~~
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
~~~
使用 Memcached 需要安裝 [dalli](https://github.com/mperham/dalli),操作時:
~~~
Rails.cache.read('key')
Rails.cache.write('key', value)
Rails.cache.fetch('key') { value }
~~~
#### 6.2.5.4 :null_store
這是一種適合開發和測試環境的配置,它不會儲存任何東西,但是可以正常調試 Rails.cache 中的方法。
~~~
config.cache_store = :null_store
~~~
#### 6.2.5.5 自定義緩存服務
[Redis](http://redis.io/) 作為一個高性能的內存型數據庫,也可以作為緩存服務。我們先安裝 redis 的 gem:
~~~
gem 'redis-rails'
gem "hiredis"
~~~
增加配置:
~~~
config.cache_store = :redis_store, {
host: 127.0.0.1,
port: 6379,
password: 123456,
db: 1,
namespace: "cache" }
~~~
現在,越來越多的 Rails 項目和 redis 配合使用,比如下一節要介紹的異步服務,還有大量非結構化的數據,也可以儲存在 redis 中。比如站內短信息,好友動態,或者好友列表,都可以通過 redis 的命令快速實現,較之關系型數據庫擁有更快的讀寫速度,且更適合儲存非結構化數據。
> 非結構化數據庫是指其字段長度可變,并且每個字段的記錄又可以由可重復或不可重復的子字段構成的數據庫,用它不僅可以處理結構化數據(如數字、符號等信息)而且更適合處理非結構化數據(全文文本、圖象、聲音、影視、超媒體等信息)。來自百度百科
### 6.2.6 緩存的讀取和寫入
我們可以在 Rails 項目內部,使用 `Rails.cache.fetch` 來讀取緩存,如果不存在,將返回 nil,如果傳入 block,會將 block 中的結果寫入緩存,并將其返回。比如:
~~~
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
~~~
在 fetch 中可以設置過期時間。
更多 API 信息可以查看 [這里](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html)。
- 寫在前面
- 第一章 Ruby on Rails 概述
- Ruby on Rails 開發環境介紹
- Rails 文件簡介
- 用戶界面(UI)設計
- 第二章 Rails 中的資源
- 應用 scaffold 命令創建資源
- REST 架構
- 深入路由(routes)
- 第三章 Rails 中的視圖
- 布局和輔助方法
- 表單
- 視圖中的 AJAX 交互
- 模板引擎的使用
- 第四章 Rails 中的模型
- 模型的基礎操作
- 深入模型查詢
- 模型中的關聯關系
- 模型中的校驗
- 模型中的回調
- 第五章 Rails 中的控制器
- 控制器中的方法
- 控制器中的邏輯
- 第六章 Rails 的配置及部署
- Assets 管理
- 緩存及緩存服務
- 異步任務及郵件發送
- I18n
- 生產環境部署
- 常用 Gem
- 寫在后面