翻译 使用 Redis 处理 Rails Model 缓存

jesktop · 2015年07月30日 · 最后由 StephenZzz 回复于 2019年10月15日 · 10691 次阅读

原文:Rails Model Caching with Redis 第一次翻译文章,估计翻译问题多,欢迎拍砖。

Model 层的缓存常常都会被忽略,甚至是经验丰富的码农。当你对视图层做缓存时,你不需要进行底层缓存,这是一个非常常见的误解。虽然在 Rails 里大部分的瓶颈在于视图层,但是总有个别情况不是这样的。

底层缓存是非常灵活的,可以工作于任何一个应用程序。在本教程中,我将演示如何使用 Redis 来缓存你的 model 层。

缓存是如何工作的?

过去,访问磁盘的成本已经非常高了。而且从磁盘访问数据经常会对性能产生不利的影响。为了解决这个问题,我们可以在应用程序和数据库服务器之间加上缓存层。

缓存层在初始化时是没有任何数据的。当它接收到数据请求时,它会调用数据库并将结果存储在内存中(缓存)。所有后续的请求将从缓存层直接读取数据,所以可以避免不重复往返的访问数据库服务器,从而提高性能。

为什么使用 Redis?

Redis 是一个基于内存、Key-Value 存储系统。它的速度极快,几乎是瞬间完成数据检索。Redis 支持先进的数据结构,如链表,哈希表,集合,并能持续保存到磁盘。

虽然大多数码农更喜欢使用 Memcache 和 Dalli 去处理他们的缓存化的需求,但我发现 Redis 非常容易的安装和方便管理。另外,如果你使用的是 resque 或 Sidekiq 管理你的队列,你很可能已经安装了 Redis 了。对于那些有兴趣了解何时使用 Redis 的朋友们,可以到这个 讨论 里了解更多相关信息。

前提

我假设你的项目正在使用 Rails,文章中的例子是使用 Rails 4.2.rc1,使用 haml 渲染视图和 MongoDB 作为数据库,但是本教程的片段应该适用于任何版本的 Rails。

在开始之前,你需要安装和运行 Redis。进入你的应用程序目录,并执行以下命令:

$ wget http://download.redis.io/releases/redis-2.8.18.tar.gz
$ tar xzf redis-2.8.18.tar.gz
$ cd redis-2.8.18
$ make

这个命令将需要一段时间才能完成。一旦完成了,就可以开启 Redis 服务了:

$ cd redis-2.8.18/src
$ ./redis-server

使用 gem “rack-mini-profiler” 可以测量性能提升,这个 gem 可以帮助我们正确的体现出性能的改善。

开始

例如,让我们构建一个虚拟的在线故事书阅读书店。这个书店有各种各样的书籍和语言。首先,让我们创建模型:

# app/models/category.rb

class Category
  include Mongoid::Document
  include Mongoid::Timestamps
  include Mongoid::Paranoia

  include CommonMeta
end

# app/models/language.rb

class Language
  include Mongoid: :Document
  include Mongoid::Timestamps
  include Mongoid::Paranoia

  include CommonMeta
end

# app/models/concerns/common_meta.rb

module CommonMeta
  extend ActiveSupport::Concern
  included do
    field :name, :type => String
    field :desc, :type => String
    field :page_title, :type => String
  end
end

我在这里包括了一个 seed 数据文件。只要复制粘贴到你的 seeds.rb 和运行 rake seed 任务,数据就会加载到我们的数据库中。

rake db:seed

现在,让我们创建一个简单的 Category 列表页面,该页面显示了所有 Categories 的描述和标记信息。

# app/controllers/category_controller.rb

class CategoryController < ApplicationController
  include CategoryHelper
  def index
    @categories = Category.all
  end
end

# app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    @categories = Category.all
  end
end

# app/views/category/index.html.haml

%h1
  Category Listing
%ul#categories
  - @categories.each do |cat|
      %li
        %h3
          = cat.name
        %p
          = cat.desc

# config.routes.rb

Rails.application.routes.draw do
  resources :languages
  resources :category
end

当你打开浏览器并将其地址指向/category 时,你会发现 mini-profiler benchmarking 显示在后端执行每一个动作的时间。这些正确的数据是告诉你,你的应用程序哪部分比较缓慢和应该如何优化它们。本页面执行了两条 SQL 语句并且使用了 5ms 的时间完成查询。

虽然起初看起来好像 5ms 是无关紧要的,特别是在需要更多时间去渲染视图时,但在一个生产级别的应用程序中有多次数据库查询时,它们可以明显的降低软件的性能。

由于元数据模型是不太可能发生改变的,这样就可以避免不必要的数据库切换。这一点也是底层缓存的用武之地。

安装 Redis

使用 Redis 基于 Ruby 的客户端来帮助我们非常方便的链接 Redis 实例:

gem 'redis'
gem 'redis-namespace'
gem 'redis-rails'
gem 'redis-rack-cache'

一但安装好这些 gem 后,就可以配置 Rails 使用 Redis 来作为缓存存储:

# config/application.rb

#...........
config.cache_store = :redis_store, 'redis://localhost:6379/0/cache', { expires_in: 90.minutes }
#.........

使用 redis-namespace gem 可以让我们创建一个更好的 Redis 命名空间:

# config/initializers/redis.rb 

$redis = Redis::Namespace.new("site_point", :redis => Redis.new)

现在所有的 Redis 功能都可以通过$redis进行全局使用了。以下的一个例子是体现如何访问在 redis 服务器上的值(运行于 Rails console):

$redis.set("test_key", "Hello World!")

这个命令创建了一个 key:“test_key” 和 value:“Hello World” 保存在 Redis 中。要取这个值,只做:

$redis.get("test_key")

现在,我们有了基础知识,让我们开始重写我们的 helper 方法:

# app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    categories =  $redis.get("categories")
    if categories.nil?
      categories = Category.all.to_json
      $redis.set("categories", categories)
    end
    @categories = JSON.load categories
  end
end

在第一次执行这部分代码时,内存/缓存中是没有任何东西的。所以我们请求 Rails 把数据从数据库推送到 Redis 中。注意到 to_json的调用了吗? 当要写对象进 Redis,我们多种方式。一种选择是遍历对象中的每个属性,然后将它们保存为一个哈希函数,但是这种方式较为缓慢。最简单的方法是将它们保存为一个 JSON 编码的字符串。解码,只需使用JSON.load

然而,这有一个意想不到的副作用。当我们正在检索这个值时,一个简易的对象符号不工作。我们需要更新视图并使用哈希语法来显示该类型:

# app/views/category/index.html.haml

%h1
  Category Listing
%ul#categories
  - @categories.each do |cat|
    %li
      %h3
        = cat["name"]
      %p
        = cat["desc"]

重新启动浏览器,并看看性能是否有所不同。首次访问,我们仍然访问数据库,但随后的重新加载将不在访问数据库了。以后所有的请求都将直接从缓存中读取。这个简单的变化非常有效。

管理缓存

我刚发现一个关于 categories 的错误。让我们先解决它:

$ rails c

c = Category.find_by :name => "Famly and Frends"
c.name = "Family and Friends"
c.save

重新加载并查看该更新是否显示在视图中:

很遗憾,我们的视图上并没有体现出这个变化。因为我们并没有访问数据库,所有的数据都直接从缓存中读取。唉,现在的缓存已经过期,直到 Redis 重启前被更新都数据都无法使用。这个对于大多数应用程序来说真是一个破坏者啊。我们偶尔可以使用缓存到期来解决这个问题:

# app/helpers/category_helper.rb

module CategoryHelper
  def fetch_categories
    categories =  $redis.get("categories")
    if categories.nil?
      categories = Category.all.to_json
      $redis.set("categories", categories)
      # Expire the cache, every 3 hours
      $redis.expire("categories",3.hour.to_i)
    end
    @categories = JSON.load categories
  end
end

缓存将会在每 3 个小时就失效。虽然这对大多数情况下工作,缓存中的数据将滞后于现在数据库。这种工作方式很可能不抬适合你。如果你喜欢保持缓存的更新,我们可以使用after_save这个回调:

# app/models/category.rb

class Category
  #...........
  after_save :clear_cache

  def clear_cache
    $redis.del "categories"
  end
  #...........
end

每次模型的更新,我们都将通知 Rails 去清除缓存。这样可以确保缓存是最新的。Yay!

你应该使用类似cache_observers在生产环境中,为了保持简洁,我们在这里坚持使用after_save。如果你不知道哪种方法最适合你,这里的讨论可能会对你有所启发。

结论

底层缓存是非常简单的,如果使用得当,它是非常有价值的。它可以在你花费最小的努力下瞬间提高你的系统的性能。在这篇文章中所有的代码片断可以在 GitHub 上找到。

希望喜欢读篇文章。欢迎在评论中分享你的想法。

译文地址:http://jesktop.com/2015/07/17/rails-model-caching-with-redis/

感谢 @superbear@javie007 协助修订校对

共收到 14 条回复

感谢楼主的无私奉献,学习了。

请问 fetch_categories 方法是怎么调用的呢?

#2 楼 @kikyous 感觉省略的东西还是不少,可参考源码:https://github.com/skmvasu/redis_cache_sitepoint/blob/master/app/controllers/category_controller.rb#L6 直接在 controller 里用fetch_categories代替@categories = Category.all

若是先对数据分页处理在缓存就比较麻烦,好像也就没必要缓存了。

为啥不用 Rails.cache.fetch?

#5 楼 @nowherekai 貌似是因为使用了namespace

identity_cache 也可以吧

学习了谢谢楼主

感谢 楼主

Zegendary 用 ActionCable + Redis 实现在线人数统计功能 中提及了此贴 11月30日 18:34

sinatra 吗

工作中也是一直这么用的,只是没有你的表达功力强👍 👍

技能点 +1,,感谢楼主

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