在 Forge - Rails 中的 Memcached 集成

作者:Reuven M. Lerner

上个月,我们讨论了 memcached,一个在网站中广泛使用的分布式缓存系统。memcached 受欢迎的原因在于其简洁性。只需最少的开销和设置,就可以设置和检索几乎任何值。缓存原本来自数据库的值使得在许多情况下可以完全避免数据库,从而加快 Web 应用程序的吞吐量并减少数据库服务器上的负载。

Memcached 是一个很棒的工具,几乎每个 Web 开发人员都应该在其武器库中拥有它,以提高网站性能。但随着 Ruby on Rails 2.1 的发布,情况变得更好了。Rails 现在集成了对 memcached 的支持,允许您几乎免费地从应用程序内部使用它。它的使用有一些注意事项和技巧,但是一旦您掌握了这些,您将很快发现 memcached 显着提高了您的网站性能。

本月,我们将了解如何在您的 Rails 应用程序内部使 memcached 工作。我们将进一步探讨您在使用 memcached 时可能遇到的一些问题,其中一些问题比其他问题更容易解决。

缓存集成

自 Rails 诞生以来,它一直试图通过推出 Web 开发人员可能需要的许多工具来使 Web 开发人员的生活更轻松。它带有一个出色的对象关系映射器 (ORM) ActiveRecord。它提供了一种在各种不同级别(在 Rails 术语中称为单元、功能和集成)测试代码的方法。它带有一个一流的 JavaScript 库和相关的效果,在 Prototype 和 Scriptaculous 中。正如许多演示和教程所展示的那样,Rails 允许您直接进入 Web 开发,以最少的依赖项编写和测试您的代码。如果您需要包含 Rails 作者遗漏的某些功能,那么包含一个 Ruby gem(可下载的库)甚至一个位于 Rails 应用程序内部的“插件”也不是很困难。

长期以来,Rails 都配备了一个多层缓存系统,程序员可以利用它来加速应用程序。您可以缓存单个页面、控制器操作甚至页面片段。实际上,明智地使用 Rails 缓存命令可以显着提高性能。

但是,直到 2.1 版本,Rails 才集成对缓存单个对象的支持。对对象缓存的支持不仅有可能显着提高应用程序的性能,而且还允许您使用各种不同的存储设施,因此您可以选择最适合您的设施。尽管本文重点介绍 memcached 的使用,但您应该知道,不仅可以使用 memcached,还可以使用本地文件系统、本地内存甚至使用 DRb(分布式 Ruby,作为 Ruby gem 提供)的另一个 Rails 感知服务器上的缓存。

缓存简单对象

为了演示如何使用 memcached,我将创建一个简单的 Rails 应用程序,使用 PostgreSQL 作为数据库

createdb atf
rails --database=postgresql atf

接下来,我为我的应用程序创建一个简单的对象 person,使用 Rails 内置的 scaffolding,其中包括 RESTful 接口

./script/generate scaffold person firstname:string 
 ↪lastname:string email_address:string

要将此定义导入数据库,我运行它创建的迁移

rake db:migrate

果然,如果我连接到数据库,我可以看到表已创建(清单 1)。

清单 1. 示例表

atf_development=# \d people
                      Table "public.people"
 Column       |            Type             | Modifiers
--------------+-----------------------------+-----------------------------------
id            | integer                     | not null default nextval
                                              ↪('people_id_seq'::regclass)
firstname     | character varying(255)      |
lastname      | character varying(255)      |
email_address | character varying(255)      |
created_at    | timestamp without time zone |
updated_at    | timestamp without time zone |
  Indexes:
      "people_pkey" PRIMARY KEY, btree (id)

而且,如果我运行该应用程序,我可以访问(通过 RESTful 接口)与 Person 对象关联的各种 CRUD 功能:创建、检索、更新和删除。我只需输入

./script/server

然后,我将 Web 浏览器指向服务器上的端口 3000:http://atf.lerner.co.il:3000/people/。

到目前为止,一切顺利。通过 UNIX 命令行上的一些命令,我成功创建了一个简单的人员数据库。我将使用 scaffolded 应用程序添加几个人,单击“新建人员”链接,然后添加我的每个朋友的名字、姓氏和电子邮件地址。

现在,如果我查看 Rails 开发日志,我可以很容易地看到,我在 scaffolded 环境中执行的每个操作都会导致构建 SQL 查询并将其发送到 PostgreSQL 服务器。我经常通过键入以下内容来执行此操作

tail -f log/development.log

例如,如果我单击为我创建的第一个人员显示的链接,我会在开发日志中看到以下内容

Person Load (0.001571)   SELECT * FROM "people" 
 ↪WHERE ("people"."id" = 1)

换句话说,Rails 知道我想加载一个 Person 对象。它也知道我从数据库中检索此类对象。这就是 ActiveRecord 介入的地方,将 Ruby 转换为

Person.find(1)

变为

SELECT * FROM people WHERE people.id = 1

您可以想象,执行这种简单的查询并不是什么大问题,特别是如果您拥有的字段数量有限、数据集较小且主键索引良好。但是,随着字段数量的增加,您可能会发现自己希望减少数据库上的负载。此外,现代动态网站可能需要从数据库中检索 5-10 个不同的对象,其中只有一些是特定于当前用户的。如果您每天甚至有 1,000 名访问者访问您的网站,并且如果每个页面上有三个可以缓存的对象,那么您不必要地向数据库发出了 3,000 个数据库查询。

Memcached 是解决此问题的显而易见的解决方案。在以前版本的 Rails 中,您需要使用插件或 Ruby gem 才能做到这一点。但是,现在,您可以通过配置文件来完成。您之前需要安装的 gem memcached-client 现在已包含在 Rails gem 中。每个 Rails 应用程序都包含一个主配置文件 (config/environment.rb),它允许您使用 Ruby 代码配置您的应用程序。这是您应该放置所有三个标准 Rails 环境(开发、测试和生产)通用的配置的地方。对于特定于某个环境的配置,您应该修改 config/environments/ENV.rb,其中 ENV 应替换为您选择的环境。

因为我们仍在开发示例应用程序,并且使用开发环境,所以我们可以将更改限制为 config/environments/development.rb。在您选择的编辑器中打开该文件,并添加以下行

config.cache_store = :mem_cache_store

这告诉 Rails 您要使用 memcached,并且服务器位于本地计算机 (localhost) 上,使用默认端口 11211。但是,您可以覆盖这些,甚至可以将内容放入单独的命名空间中,如果您担心踩到别人的对象。

当您在开发模式下工作时,您还需要告诉服务器使用缓存,这是一个默认设置(并且为 false)的参数

config.action_controller.perform_caching = true
缓存对象

现在,让我们进入并修改 scaffolding 系统为我们构建的控制器中的 GET 操作。(内置缓存旨在从控制器和视图而不是从模型中使用。)这将是

app/controllers/people_controller.rb

在该文件的第 16 行,您将看到

@person = Person.find(params[:id])

这显然是我们调用 Person.find 的地方,如之前的日志所示。现在,修改该行,使其看起来像这样

@person = cache(['Person', params[:id]]) do
  Person.find(params[:id])
end

我们仍然在为 @person 赋值。而且,我们对 Person.find 的调用仍然在那里。但是,Person.find 现在被埋在一个块中。并且,该块附加到对缓存函数的调用,该函数被赋予一个数组参数。

这里发生的事情实际上非常简单。缓存函数在其参数的缓存中查找,该参数被转换为键。如果缓存中存在此键的值,则返回该值。否则,将执行该块,并将执行该块的结果存储在缓存中并返回给调用者。

有了这段代码,让我们再次检索 person #1 并查看日志文件。我们第一次这样做时,该值确实像以前一样从数据库中检索

Person Load (0.002212)   SELECT * FROM "people" 
 ↪WHERE ("people"."id" = 1)

该行之后是这个新条目

Cache write (will save 0.01852): controller/Person/1

果然,我们的 memcached 服务器报告

<7 new client connection
<7 get controller/Person/1
>7 END
<7 set controller/Person/1 0 0 224
>7 STORED

换句话说,我们的 Rails 控制器完全按照我们的要求执行。它联系了 memcached 并询问 controller/Person/1 的值。(我们可以从中看出控制器被前置到我们创建的键名,并且缓存键数组的元素用斜杠分隔。)当我们获得该值的空值时,Rails 从数据库中检索该值,然后在 memcached 中发出 set 命令,存储我们的值。

正如您可能预期的那样,然后我们可以刷新浏览器窗口,并看到我们通过从缓存中检索有关此人的信息来节省大量数据库时间。所以,我们刷新浏览器窗口,然后……砰!我们的应用程序在我们身上爆炸了,并出现了一条错误消息,如下所示

undefined class/module Person

现在,当我第一次遇到这种情况时,我不确定发生了什么。你是说,我问我的电脑,你不知道如何找到 Person 类?一番挠头和 Google 搜索之后,我找到了答案。我需要通过将以下内容放在控制器的顶部来告诉控制器加载对象定义

require_dependency 'person'

这显然仅在开发模式下是必需的,并且它与 Rails 在您开发应用程序时重新加载类的方式有关。有了该行,您可以重新加载页面。在日志文件中,您将看不到成功调用数据库的痕迹。相反,您会发现以下内容

Cache hit: controller/Person/1 ({})

与此同时,我们的 memcached 日志将如下所示

<7 get controller/Person/1
>7 sending key controller/Person/1
>7 END

现在是时候提到我能想到的唯一其他陷阱了:memcached 键中禁止使用空格。如果您使用数据库中的值(例如,参数名称)作为在 memcached 中存储内容的键,这可能会成为问题。简单的解决方案是删除空格,可以通过在每个键上运行 String#gsub 或通过 monkey-patching String(就像我在编写的应用程序中所做的那样)来添加 to_key 方法。然后我可以传递"parameter name".to_mkey作为 cache() 的参数。

过期

现在,我们已经在 memcached 中缓存了有关每个人的信息,这都很好。我们的数据库肯定会为此感谢我们。但是,当有关人员的数据发生变化时会发生什么?按照我们编写此应用程序的方式,我们很不幸。更新后的信息将进入数据库,但缓存将继续为我们提供很久以前存储的数据。即使情况并非如此,我们仍然希望偶尔清空缓存,以便如果我们一段时间未使用数据,则允许数据过期。

为了解决第二个问题,我们可以以稍微不同的方式调用我们的缓存函数,指示我们希望它在第二个(和可选)参数中保留多长时间

@person = cache(['Person', params[:id]],
                :expires_in => 30.minutes) do
  Person.find(params[:id])
end

:expires_in 参数接受秒数,我们可以手动输入秒数,也可以通过 Fixnum 类的超级方便的 Rails 扩展之一输入秒数。

第二个问题,即手动使数据过期的问题,要求我们使用一种不太美观但也很方便的方式来访问缓存存储系统

Rails.cache.delete(['controller', 'Person', 
 ↪params[:id]].join('/'))

基本上,我们使用 Rails.cache 对象访问缓存系统,并在其上调用 delete 方法。该方法接受 memcached 键。您可能还记得,我们之前看到我们的键数组(由有用的缓存方法使用)的元素用斜杠连接,并以 controller 为前缀。因此,即使它没有我可能希望的那么好,上面的方法也有效。我们可以从 memcached 日志中看到情况就是如此

<7 delete controller/Person/1 0
>7 DELETED

果然,我们随后发现,我们下次为 person 1 调用 show 时,会从数据库中检索信息并将其缓存在 memcached 中。

结论

长期以来,缓存一直是提高计算机行业性能的绝佳方法,从硬件级别一直到操作系统和应用程序。Rails 程序员在过去几年中已将 memcached 集成到他们的应用程序中,但我相信它在 2.1 版本中的完全集成将使 memcached 支持的 Rails 应用程序更容易找到和更广泛地应用。如您所见,只需添加几行配置和应用程序代码即可将应用程序速度提高数倍,而无需牺牲准确性。

资源

如果您正在寻找有关 memcached 的信息,您应该从 www.danga.com/memcached 开始,这是开源项目的主页,也是大量优秀文档、代码和一般信息的来源。

有关 Ruby on Rails 的信息,请从访问 www.rubyonrails.com 开始,其中包含指向文档、邮件列表和(当然)您可以下载的软件的指针。

有关 memcached 集成到 Rails 中的信息,请尝试 www.thewebfellas.com/blog/2008/6/9/rails-2-1-now-with-better-integrated-caching

有一些 Rails 插件可能会使缓存对象更容易。例如,看看 www.inwebwetrust.net/post/2008/09/08/query-memcachedlucaguidi.com/pages/cached_models,自从 Rails 2.1 缓存发布以来,这两者都引起了一些关注。

最后,关于 memcached 与 Rails 一起使用的教程包含在 Pragmatic Programmers 出版的 Advanced Rails Recipes 的一章中。我非常喜欢这本书,并将其推荐给任何计划将 Rails 用于不仅仅是简单应用程序的人。关于 memcached 的章节已作为免费样本发布,并且可以 PDF 格式在 media.pragprog.com/titles/fr_arr/cache_data_easily.pdf 中获得。

Reuven M. Lerner 是一位长期的 Web/数据库开发人员和顾问,是西北大学学习科学专业的博士候选人,研究在线学习社区。在芝加哥地区生活了四年之后,他最近(与他的妻子和三个孩子)返回了他们在以色列莫迪因的家。

加载 Disqus 评论