rails 中就自带有 cache 功能,不过它默认是用文件来存储数据的。我们要改为使用 redis 来存储。而且我们也需要把 sessions 也存放到 redis 中。关于 rails 实现 cache 功能的源码可见于这几处:
我们一步步在 rails 中使用 cache 实现我们的需求。
首先第一步我们要来开启 cache 模式。默认情况下,production 环境是开启的,但是 development 没有,所以要开启它。
进入config/environments/development.rb
文件中,把config.action_controller.perform_caching
设为 true。
config.action_controller.perform_caching = true
修改完,记得重启服务器。
为了方便测试和了解整个原理,我们先不使用 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 会自动更新吗?
先来说更改 html 片断代码本身的情况。
我们把"最近的文章"改成”最新的文章",然后我们来观察是否会生效。
最终通过查看日志,发现还是产生了Write fragment
,说明是生效的。
这个原理是什么呢?
我们找到 cache 这个 helper 方法的源码。
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)。
完结
原文链接:http://www.rails365.net/articles/2015-10-30-redis-shi-xian-cache-xi-tong-shi-jian-liu