Rails Redis 实现 Cache 系统实践

hfpp2012 · 发布于 2015年11月04日 · 最后由 pathbox 回复于 2016年04月17日 · 5635 次阅读
16154
本帖已被设为精华帖!

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

共收到 17 条回复
14329

好贴!先 mark 午休看!

11314

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

20521

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

16154

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

5178

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

A87c18

#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数据持久化到磁盘。

20521

@hfpp2012 @zhang_soledad 受教了,谢谢各位。

20521

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

16154

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

23844
谢谢学习了
24049

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

14楼 已删除
15420

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

16楼 已删除
17楼 已删除
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册