世界上最伟大的投资就是投资自己的教育

首页Redis
随风 · 练气

Redis 学习笔记系列文章之 Redis 实现 cache 系统实践 (六)

随风发布于3083 次阅读

1. 介绍

rails 中就自带有 cache 功能,不过它默认是用文件来存储数据的。我们要改为使用 redis 来存储。而且我们也需要把 sessions 也存放到 redis 中。关于 rails 实现 cache 功能的源码可见于这几处:

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 digestRead fragmentWrite 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 方法的源码

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)。

完结

下一篇:Redis 学习笔记系列文章之 Redis 实现消息队列 (七)

本站文章均为原创内容,如需转载请注明出处,谢谢。

0 条回复
暂无回复~~
相关小书
redis入门与应用教程

redis入门与应用教程

redis从入门开始讲起,通过应用来深入理解redis

发表于

喜欢
统计信息
    学员: 30007
    视频数量: 1996
    文章数量: 526

© 汕尾市求知科技有限公司 | Rails365 Gitlab | 知乎 | b 站 | csdn

粤公网安备 44152102000088号粤公网安备 44152102000088号 | 粤ICP备19038915号

Top