李玮 · 更新于 2018-11-28 11:00:42

6.2 缓存

概要:

本课时讲解 Rails 中如何使用缓存。

知识点:

  1. 缓存
  2. redis
  3. memcached

正文

6.2.1 Rails 缓存

Rails 提供了三种方式的缓存,页面缓存,方法缓存和片段缓存,在 Rails 4 之前的版本里,它包含在 Rails 中,但是从 4.x 开始,三种缓存中的两种转为 gem 形式,只有片段缓存保留在 Rails 默认 中。

在开发环境下,缓存是关闭的,如果要测试它,需要更改配置:

config.action_controller.perform_caching = true

在产品环境下,它默认是 true。

6.2.2 页面缓存,Page Cache

Rails 4.x 将页面缓存转为 gem,使用的时候需要加入到 gemfile 中。

我们设置一下缓存路径,在 config/environments/development.rb

config.action_controller.page_cache_directory = "#{Rails.root.to_s}/public"

页面缓存是将整个页面,生成一份静态的 html 页面,这个页面会保存在刚才设置的目录中。Rails 在显示该地址的时候,会优先查找 public 是否有同名的 html 文件优先显示。

我们把 show 方法加入到页面缓存中:

class ProductsController < ApplicationController
  ...
  caches_page :show

当第一次访问时,会创建该缓存文件:

Write page /path/to/project/public/products/3.html (9.5ms)

再次访问时,便直接读取该文件,而不再执行 show 方法了。

这样做的好处是,可以把一些经常访问的页面作为页面缓存。缺点是,这种页面不能有太多用户的个人信息,因为这个页面对所有人访问都是相同的内容。如果必须考虑个人信息,可以改为 js 形式,或者使用方法缓存(Action Cache)。

当这个缓存页面内容更改时,可以删掉该文件,再次访问时会自动创建。也可以在 update 内加入过期的命令:

def update
  respond_to do |format|
    if @product.update(product_params)
      expire_page action: 'show', id: @product.id
      ...
    else
      ...
    end
  end
end

更新资料后会自动过期该文件。

Expire page /path/to/project/public/products/3.html (1.0ms)

6.2.3 方法缓存,Action Cache

方法缓存和页面缓存的区别是:它会执行对应的 action 中的代码。页面缓存直接读取缓存文件,不执行 action 中的代码。

页面缓存的 gem 在这里。

我们给方法增加方法缓存:

class ProductsController < ApplicationController
  ...
  caches_action :index, layout: false

访问该页面,会创建一个片段缓存(fragment cache)文件:

Write fragment views/localhost:3000/products (5.9ms)

该片段缓存为当前整个页面,我们增加 layout: false 参数,这样,片段缓存只包含该 action 对应的模板内容,而不包含 layout。我们设计的代码,将用户信息放置在 layout 中,登录后会显示用户名。所以 layout 是不应该放到缓存中的。

但是,因为我们给 index 方法增加了搜索功能,而该方法已经加入到了缓存中,所以,搜索是还是显示的缓存内容。这里可以做调整,要么将搜索放到专用的非缓存方法中,要么搜索时过时该缓存。

6.2.4 片段缓存,Fragment Cache

片段缓存,是 Rails 默认使用的缓存方式,它指的是视图(view)中,缓存局部内容:

<% cache do %>
  分类:
  <% Catalog.all.each do |catalog| %>
    <%= link_to catalog.name, catalog %>
  <% end %>
<% end %>

这里把经常访问的分类列表,加入到了缓存中,避免每次页面访问该部分都读取数据库。

我们可以给 cache 方法增加一些参数:

<% cache(action: 'new', action_suffix: 'all_products') do %>

它产生的缓存 key 是:

Write fragment views/localhost:3000/products/new?action_suffix=all_products/02c540e3ab26f72d5e9273d5824c204e (60.0ms)

也可以直接命名缓存 key:

<% cache( "all_products" ) do %>

它产生的缓存 key 是:

Write fragment views/all_products/cc926a692262d0e538f07d5dd5d54942 (15.1ms)

或者直接缓存一个实例:

<% cache @product do %>

它产生的缓存 key 是:

Write fragment views/products/3-20150620164035711340000/b0699b1b8be94ebd1bfcfe74a21571f8 (21.5ms)

可见,缓存是产生一个 key: value 结构的数据。key 来自于实例的 cache_key 方法:

p = Product.last
p.cache_key
 => "products/3-20150620164035711340000" 
p.updated_at = nil
p.cache_key
 => "products/3" 

该方法会读取 updated_at 字段值,这样,每当该实例更改的时候,会自动更新 updated_at 字段,相当于自动更新了缓存。

我们可以使用

expire_fragment(action: 'new', action_suffix: 'all_products')
expire_fragment("all_products")

过期这些片段缓存

6.2.5 缓存服务

缓存产生的是 key: value 结构的数据,所以我们可以使用支持该解构的数据库来保存缓存。在 config/environments/production.rb 中有 cache_store 的选项:

# Use a different cache store in production.
config.cache_store = :mem_cache_store

这里有四个选项可以使用::memory_store, :file_store, :mem_cache_store, :null_store。在 手册里还介绍了 JRuby 的 Ehcache。

6.2.5.1 :memory_store

缓存和 Ruby 进程使用共同的内存,默认大小为32M,如果超出这个范围,会移除掉旧的记录。我们可以更改这个限制:

config.cache_store = :memory_store, { size: 64.megabytes }

但是多个 Rails 应用不会共享该缓存。它不适合大型的部署,适合小型的,低访问量的应用。

6.2.5.2 :file_store

config.cache_store = :file_store, "/path/to/cache/directory"

缓存利用文件系统来存放缓存文件,虽然可以在多个应用间共享缓存,但是不建议在产品环境下使用。这种方式会不断的增加硬盘使用,直到手动清空所有缓存。

Rails 默认使用这种方式。

6.2.5.3 :mem_cache_store

这种方式使用 Memcached 最为后端缓存服务,它提供了高性能的、集中式的缓存服务,可以在多个应用间共享缓存,这是一种适合中大型商业应用的选择。

config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"

使用 Memcached 需要安装 dalli,操作时:

Rails.cache.read('key')
Rails.cache.write('key', value)
Rails.cache.fetch('key') { value }

6.2.5.4 :null_store

这是一种适合开发和测试环境的配置,它不会储存任何东西,但是可以正常调试 Rails.cache 中的方法。

config.cache_store = :null_store

6.2.5.5 自定义缓存服务

Redis 作为一个高性能的内存型数据库,也可以作为缓存服务。我们先安装 redis 的 gem:

gem 'redis-rails'
gem "hiredis"

增加配置:

config.cache_store = :redis_store, {
      host: 127.0.0.1,
      port: 6379,
      password: 123456,
      db: 1,
      namespace: "cache" }

现在,越来越多的 Rails 项目和 redis 配合使用,比如下一节要介绍的异步服务,还有大量非结构化的数据,也可以储存在 redis 中。比如站内短信息,好友动态,或者好友列表,都可以通过 redis 的命令快速实现,较之关系型数据库拥有更快的读写速度,且更适合储存非结构化数据。

非结构化数据库是指其字段长度可变,并且每个字段的记录又可以由可重复或不可重复的子字段构成的数据库,用它不仅可以处理结构化数据(如数字、符号等信息)而且更适合处理非结构化数据(全文文本、图象、声音、影视、超媒体等信息)。来自百度百科

6.2.6 缓存的读取和写入

我们可以在 Rails 项目内部,使用 Rails.cache.fetch 来读取缓存,如果不存在,将返回 nil,如果传入 block,会将 block 中的结果写入缓存,并将其返回。比如:

class Product < ActiveRecord::Base
  def competing_price
    Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
      Competitor::API.find_price(id)
    end
  end
end

在 fetch 中可以设置过期时间。

更多 API 信息可以查看 这里