Rails Redis 实现 Cache 系统实践

hfpp2012 · 2015年11月04日 · 最后由 StephenZzz 回复于 2019年11月12日 · 9806 次阅读
本帖已被管理员设置为精华贴

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

完结

原文链接:http://www.rails365.net/articles/2015-10-30-redis-shi-xian-cache-xi-tong-shi-jian-liu

好贴!先 mark 午休看!

有一次我遇到这么个情况:页面想片段缓存,而又不想去再次执行action的代码,我的做法是在 action里面去判断键是否存在来决定是否执行相关action代码,清缓存根据逻辑去处理就行了。

用 redis 做缓存,比用 memcached 做缓存有什么优势么?

#3 楼 @happyming9527 用 redis 支持更丰富的数据结构,且能持久化,而且 redis 性能也好

#3 楼 @happyming9527 性能上差不多 redis 还要优胜一些 memcached 有个地方不是很方便 不支持 key 的枚举 dalli 也不提供 delete_matched 的实现 使用多有不便

#3 楼 @happyming9527 rubyweekly 有一篇文章是专门介绍缓存的,写的非常好。其中一部分也是介绍到了 redis 和 memcached 的优缺点。 http://www.nateberkopec.com/2015/07/15/the-complete-guide-to-rails-caching.html?utm_source=rubyweekly&utm_medium=email#memcache-and-dalli

redis 对比 memcached memcached 缺点:

Cache values are limited to 1MB. In addition, cache keys are limited to 250 bytes.

redis 的优点:

Allows different eviction policies beyond LRU Redis allows you to select your own eviction policies, which gives you much more control over what to do when the cache store is full. For a full explanation of how to choose between these policies, check out the excellent Redis documentation.

redis 支持的数据结构类型非常丰富,而且支持多种缓存的失效策略。

Can persist to disk, allowing hot restarts Redis can write to disk, unlike Memcache. This allows Redis to write the DB to disk, restart, and then come back up after reloading the persisted DB. No more empty caches after restarting your cache store!

支持持久化操作,可以进行日志 aof 及 rdb 数据持久化到磁盘。

@hfpp2012 不知阿里云的键值存储 KVStore(支持 redis 协议),和缓存服务 ocs(兼容 memcache) 比起来,是否也会快一些呢?

#9 楼 @happyming9527 这个我没用过,就不太清楚了

谢谢学习了

相对于 cache @articles, 下面这种方法生成的 cache key 更简洁一些: cache [:articles, @articles.max(&:updated_at)]

14 楼 已删除

如果不使用 redis,每次还是会产生数据库查询。可以认为片段缓存,是减轻了再次渲染页面所耗的性能,而数据库查询还是有的。但这个数据库查询的目的是为了匹配或创建 cache key,而不是再次获取数据等

16 楼 已删除
17 楼 已删除
spike76 聊聊 Rails 中的 Cache Stores 提及了此话题。 12月18日 09:46
需要 登录 后方可回复, 如果你还没有账号请 注册新账号