# 6. redis 實現 cache 系統實踐
#### 1. 介紹
rails中就自帶有cache功能,不過它默認是用文件來存儲數據的。我們要改為使用redis來存儲。而且我們也需要把sessions也存放到redis中。關于rails實現cache功能的源碼可見于這幾處:
- [https://github.com/rails/rails/blob/master/activesupport/lib/active\_support/cache.rb](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/cache.rb)
- [https://github.com/rails/rails/tree/master/activesupport/lib/active\_support/cache](https://github.com/rails/rails/tree/master/activesupport/lib/active_support/cache)
- [https://github.com/rails/rails/blob/master/actionview/lib/action\_view/helpers/cache\_helper.rb](https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/cache_helper.rb)
#### 2. 使用
我們一步步在rails中使用cache實現我們的需求。
##### 2.1 開啟cache模式
首先第一步我們要來開啟cache模式。默認情況下,production環境是開啟的,但是development沒有,所以要開啟它。
進入`config/environments/development.rb`文件中,把`config.action_controller.perform_caching`設為true。
```
config.action_controller.perform_caching = true
```
修改完,記得重啟服務器。
##### 2.2 使用html片斷cache
為了方便測試和了解整個原理,我們先不使用redis來存放cache數據,只使用默認的文件來存放數據。
以本站為例,我們要把首頁的"最近的文章"那部分加上html片斷的cache。
使用html片斷cache,rails提供了一個helper方法可以辦到,很簡單,只需要把需要的html用cache包起來。
```
.row
- cache do
.col-md-6
.panel.panel-default
.panel-heading
div 最近的文章
.panel-body
- @articles.each do |article|
p.clearfix
span.pull-right = datetime article.created_at
= link_to article.title, article_path(article)
```
我們先在頁面刷新一下,然后通過日志來觀察。
先發現訪問起來比平時慢一點點,因為它在把cache存到文件中,具體的log是下面這樣的。
```
Started GET "/" for 127.0.0.1 at 2015-10-30 16:19:27 +0800
Processing by HomeController#index as HTML
Cache digest for app/views/home/index.html.slim: 8e89c7a7d1da1d9719fca4639859b19d
Read fragment views/localhost:4000//8e89c7a7d1da1d9719fca4639859b19d (0.3ms)
Article Load (2.0ms) SELECT "articles"."title", "articles"."created_at", "articles"."published", "articles"."group_id", "articles"."slug", "articles"."id" FROM "articles" WHERE "articles"."published" = $1 ORDER BY id DESC LIMIT 10 [["published", "t"]]
Group Load (3.5ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" IN (1, 5, 3, 4)
Article Load (0.9ms) SELECT "articles"."title", "articles"."created_at", "articles"."published", "articles"."group_id", "articles"."slug", "articles"."id", "articles"."visit_count" FROM "articles" WHERE "articles"."published" = $1 ORDER BY visit_count DESC LIMIT 10 [["published", "t"]]
Group Load (2.3ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" IN (1, 3, 4)
Group Load (4.4ms) SELECT "groups".* FROM "groups"
Write fragment views/localhost:4000//8e89c7a7d1da1d9719fca4639859b19d (41.7ms)
...
```
主要就是`Cache digest`、`Read fragment`和`Write fragment`部分。
`Cache digest`是產生一個md5碼,這個碼來標識html的片斷,會很有用,我們等下再來細說。
`Read fragment`是讀取html片斷(以文件形式存儲),根據之前產生的md5標識,發現不存在,就會生成一個html片斷并存起來,就是`Write fragment`。
默認情況下,產生的html片斷文件是存在/tmp/cache目錄里的,如下:
```
~/codes/rails365 (master) $ tree tmp/cache/
tmp/cache/
├── 20B
│ └── 6F1
│ └── views%2Flocalhost%3A4000%2F%2F8e89c7a7d1da1d9719fca4639859b19d
```
打開`views%2Flocalhost%3A4000%2F%2F8e89c7a7d1da1d9719fca4639859b19d`這個文件,就會發現里面存儲的就是html的片斷。
現在我們在刷新一遍頁面,再來看看日志。
```
Started GET "/" for 127.0.0.1 at 2015-10-30 16:53:18 +0800
Processing by HomeController#index as HTML
Cache digest for app/views/home/index.html.slim: 8e89c7a7d1da1d9719fca4639859b19d
Read fragment views/localhost:4000//8e89c7a7d1da1d9719fca4639859b19d (0.3ms)
...
```
就會發現`Write fragment`沒有了,也不查詢數據庫了,數據都從html片斷cache取了。
這樣還不算完成。我們要考慮一個問題,就是我們改了數據或html的內容的時候,cache會自動更新嗎?
##### 2.3 Cache digest
先來說更改html片斷代碼本身的情況。
我們把"最近的文章"改成”最新的文章",然后我們來觀察是否會生效。
最終通過查看日志,發現還是產生了`Write fragment`,說明是生效的。
這個原理是什么呢?
我們找到cache這個helper方法的[源碼](https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/cache_helper.rb)。
```
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
else
yield
end
nil
end
```
發現關鍵在`cache_fragment_name`這個方法里,順應地找到下面兩個方法。
```
def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil)
if skip_digest
name
else
fragment_name_with_digest(name, virtual_path)
end
end
def fragment_name_with_digest(name, virtual_path) #:nodoc:
virtual_path ||= @virtual_path
if virtual_path
name = controller.url_for(name).split("://").last if name.is_a?(Hash)
digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
[ name, digest ]
else
name
end
end
```
關鍵就在`fragment_name_with_digest`這個方法里,它會找到cache的第一個參數,然后用這個參數的內容生成md5碼,我們剛才改變了html的內容,也就是參數改變了,md5自然就變了,md5一變就得重新生成html片斷。
所以cache方法的第一個參數是關鍵,它的內容是判斷重不重新產生html片斷的依據。
改變html片斷代碼之后,是會重新生成html片斷的,但如果是在articles中增加一條記錄呢?通過嘗試發現不會重新生成html片斷的。
那我把@artilces作為第一個參數傳給cache方法。
```
.row
- cache @articles do
.col-md-6
.panel.panel-default
.panel-heading
div 最近的文章
.panel-body
- @articles.each do |article|
p.clearfix
span.pull-right = datetime article.created_at
= link_to article.title, article_path(article)
```
發現生成了`Write fragment`,說明是可以的,頁面也會生效。
```
Cache digest for app/views/home/index.html.slim: 1c628fa3d96abde48627f8a6ef319c1c
Read fragment views/articles/15-20151027051837664089000/articles/14-20151030092311065810000/articles/13-20150929153356076334000/articles/12-20150929144255631082000/articles/11-20151027064325237540000/articles/10-20150929153421707840000/articles/9-20150929123736371074000/articles/8-20150929144346413579000/articles/7-20150929144324012954000/articles/6-20150929144359736164000/1c628fa3d96abde48627f8a6ef319c1c (0.1ms)
Write fragment views/articles/15-20151027051837664089000/articles/14-20151030092311065810000/articles/13-20150929153356076334000/articles/12-20150929144255631082000/articles/11-20151027064325237540000/articles/10-20150929153421707840000/articles/9-20150929123736371074000/articles/8-20150929144346413579000/articles/7-20150929144324012954000/articles/6-20150929144359736164000/1c628fa3d96abde48627f8a6ef319c1c (75.9ms)
Article Load (2.6ms) SELECT "articles"."title", "articles"."created_at", "articles"."updated_at", "articles"."published", "articles"."group_id", "articles"."slug", "articles"."id", "articles"."visit_count" FROM "articles" WHERE "articles"."published" = $1 ORDER BY visit_count DESC LIMIT 10 [["published", "t"]]
```
但是,除此之外,還有sql語句生成,而且以后的每次請求都有,即使命中了cache,因為@articles作為第一個參數,它的內容是要通過數據庫來查找的。
那有一個解決方案是這樣的:把@articles的內容也放到cache中,這樣就不用每次都查找數據庫了,而一旦有update或create數據的時候,就讓@articles過期或者重新生成。
為了方便測試,我們先把cache的存儲方式改為用redis來存儲數據。
添加下面兩行到Gemfile文件,執行`bundle`。
```
gem 'redis-namespace'
gem 'redis-rails'
```
在`config/application.rb`中添加下面這一行。
```
config.cache_store = :redis_store, {host: '127.0.0.1', port: 6379, namespace: "rails365"}
```
`@articles`的內容要改為從redis獲得,主要是讀redis中健為`articles`的值。
```
class HomeController < ApplicationController
def index
@articles = Rails.cache.fetch "articles" do
Article.except_body_with_default.order("id DESC").limit(10).to_a
end
end
end
```
創建或生成一條article記錄,都要讓redis的數據無效。
```
class Admin::ArticlesController < Admin::BaseController
...
def create
@article = Article.new(article_params)
Rails.cache.delete "articles"
...
end
end
```
這樣再刷新兩次以上,就會發現不再查數據庫了,除非添加或修改了文章(article)。
完結