Ruby on Rails 是一个用于快速构建业务线应用程序的有用框架。但是,随着我们的应用程序增长,我们面临扩展挑战。我们可以使用各种工具,但向我们的应用程序添加不同的技术会增加复杂性。本文探讨如何使用 Redis 内存数据结构存储作为一种多用途工具来解决不同的问题。
首先,我们需要安装 Redis,这可以使用 brew
、apt-get
或 docker
完成。当然,我们需要安装 Ruby on Rails。作为一个例子,我们将构建一个在线活动管理应用程序。以下是基本模型。
class User < ApplicationRecord
has_many :tickets
end
class Event < ApplicationRecord
has_many :tickets
end
class Ticket < ApplicationRecord
belongs_to :user
belongs_to :event
end
Redis 作为缓存
应用程序的首要需求是显示一个活动售出了多少张票以及赚了多少钱。我们将创建这些方法。
class Event < ApplicationRecord
def tickets_count
tickets.count
end
def tickets_sum
tickets.sum(:amount)
end
end
这段代码将对我们的数据库执行 SQL 查询以获取数据。问题是随着规模扩大,它可能会变慢。为了加速,我们可以缓存这些方法的结果。首先,我们需要为我们的应用程序启用 Redis 缓存。将 gem 'redis-rails'
添加到 Gemfile 并运行 bundle install
。在 config/environments/development.rb
中,配置
config.cache_store = :redis_store, {
expires_in: 1.hour,
namespace: 'cache',
redis: { host: 'localhost', port: 6379, db: 0 },
}
指定 cache
命名空间是可选的,但它有所帮助。这段代码还将默认应用程序级别过期时间设置为一小时,届时 Redis 生存时间 (TTL) 将清除陈旧数据。现在我们可以将我们的方法包装在 cache
代码块中。
class Event < ApplicationRecord
def tickets_count
Rails.cache.fetch([cache_key, __method__], expires_in: 30.minutes) do
tickets.count
end
end
def tickets_sum
Rails.cache.fetch([cache_key, __method__]) do
tickets.sum(:amount)
end
end
end
Rails.cache.fetch
将检查 Redis 中是否存在特定键。如果键存在,它将返回与该键关联的值给应用程序,并且不执行代码。如果键不存在,Rails 将运行代码块中的代码并将数据存储在 Redis 中。cache_key
是 Rails 提供的一个方法,它将组合模型名称、主键和上次更新的时间戳以创建一个唯一的 Redis 键。我们将添加 __method__
,它将使用特定方法的名称来进一步唯一化键。我们可以选择性地为某些方法指定不同的过期时间。Redis 中的数据将如下所示。
{"db":0,"key":"cache:events/1-20180322035927682000000/tickets_count:","ttl":1415,
"type":"string","value":"9",...}
{"db":0,"key":"cache:events/1-20180322035927682000000/tickets_sum:","ttl":3415,
"type":"string","value":"127",...}
{"db":0,"key":"cache:events/2-20180322045827173000000/tickets_count:","ttl":1423,
"type":"string","value":"16",...}
{"db":0,"key":"cache:events/2-20180322045827173000000/tickets_sum:","ttl":3423,
"type":"string","value":"211",...}
在这种情况下,活动 1 售出九张票,总计 127 美元,活动 2 售出 16 张票,总计 211 美元。
缓存失效
如果在我们缓存此数据后立即售出另一张票会怎么样?网站将显示缓存的内容,直到 Redis 使用 TTL 清除这些键。在某些情况下,显示陈旧内容可能还可以,但我们希望显示准确、当前的数据。这就是使用上次更新时间戳的地方。我们将指定从子模型 (ticket
) 到父模型 (event
) 的 touch: true
回调。Rails 将触摸 updated_at
时间戳,这将强制为 event
模型创建一个新的 cache_key
。
class Ticket < ApplicationRecord
belongs_to :event, touch: true
end
# data in Redis
{"db":0,"key":"cache:events/1-20180322035927682000000/tickets_count:","ttl":1799,
"type":"string","value":"9",...}
{"db":0,"key":"cache:events/1-20180322035928682000000/tickets_count:","ttl":1800,
"type":"string","value":"10",...}
...
模式是:一旦我们创建了缓存键和内容的组合,我们就不会更改它。我们使用新键创建新内容,并且先前缓存的数据保留在 Redis 中,直到 TTL 清除它。这会浪费一些 Redis RAM,但它简化了我们的代码,并且我们不需要编写特殊的回调来清除和重新生成缓存。
我们需要小心选择我们的 TTL,因为如果我们的数据频繁更改且 TTL 较长,我们将存储太多未使用的缓存。如果数据不经常更改但 TTL 太短,即使没有任何更改,我们也会重新生成缓存。以下是我写的一些关于如何平衡这一点的 建议。
注意事项:缓存不应是权宜之计。我们应该寻找编写高效代码和优化数据库索引的方法。但有时缓存仍然是必要的,并且可以成为为更复杂的重构争取时间的快速解决方案。
Redis 作为队列
下一个需求是为一个或多个活动生成报告,显示每个活动收到的金额的详细统计信息,并列出已售出的带有用户信息的所有票。
class ReportGenerator
def initialize event_ids
end
def perform
# query DB and output data to XLSX
end
end
生成这些报告可能很慢,因为必须从多个表中收集数据。与其让用户等待响应并下载电子表格,不如将其转换为后台作业,并在完成后发送电子邮件,其中包含附件或文件链接。
Ruby on Rails 有一个 Active Job 框架,可以使用各种队列。在这个例子中,我们将利用 Sidekiq 库,它将数据存储在 Redis 中。将 gem 'sidekiq'
添加到 Gemfile 并运行 bundle install
。我们还将使用 sidekiq-cron
gem 来安排定期作业。
# in config/environments/development.rb
config.active_job.queue_adapter = :sidekiq
# in config/initializers/sidekiq.rb
schedule = [
{'name' => MyName, 'class' => MyJob, 'cron' => '1 * * * *',
'queue' => default, 'active_job' => true }
]
Sidekiq.configure_server do |config|
config.redis = { host:'localhost', port: 6379, db: 1 }
Sidekiq::Cron::Job.load_from_array! schedule
end
Sidekiq.configure_client do |config|
config.redis = { host:'localhost', port: 6379, db: 1 }
end
请注意,我们正在为 Sidekiq 使用不同的 Redis 数据库。这不是必需的,但如果我们需要刷新缓存,将缓存存储在单独的 Redis 数据库(甚至在不同的服务器上)可能会很有用。
我们还可以为 Sidekiq 创建另一个配置文件,以指定它应该监视哪些队列。我们不希望有太多队列,但只有一个队列可能会导致它被低优先级作业阻塞并延迟高优先级作业的情况。在 config/sidekiq.yml
中
---
:queues:
- [high, 3]
- [default, 2]
- [low, 1]
现在我们将创建作业并指定低优先级队列。
class ReportGeneratorJob < ApplicationJob
queue_as :low
self.queue_adapter = :sidekiq
def perform event_ids
# either call ReportGenerator here or move the code into the job
end
end
我们可以选择性地设置不同的队列适配器。Active Job 允许我们在同一应用程序中为不同的作业使用不同的队列后端。我们可能有每天需要运行数百万次的作业。Redis 可以处理这个问题,但我们可能想使用不同的服务,例如 AWS Simple Queue Service (SQS)。我写了一篇关于不同队列选项的 比较,可能对您有帮助。
Sidekiq 利用了许多 Redis 数据类型。它使用列表来存储作业,这使得排队非常快。它使用排序集来延迟作业执行(无论是应用程序专门请求的还是在重试时执行指数退避)。Redis 哈希存储有关执行了多少作业以及它们花费了多长时间的统计信息。
定期作业也存储在哈希中。我们可以使用普通的 Linux cron 来启动作业,但这会在我们的系统中引入单点故障。使用 Sidekiq-cron,计划存储在 Redis 中,并且运行 Sidekiq worker 的任何服务器都可以执行作业(该库确保只有一个 worker 会在计划的时间抓取特定的作业)。Sidekiq 还有一个很棒的 UI,我们可以在其中查看各种统计信息,并暂停计划的作业或按需执行它们。
Redis 作为数据库
最后一个业务需求是跟踪每个活动页面的访问量,以便我们可以确定它们的受欢迎程度。为此,我们将使用排序集。我们可以直接创建 REDIS_CLIENT
以调用本机 Redis 命令,或者使用 Leaderboard gem,它提供额外的功能。
# config/initializers/redis.rb
REDIS_CLIENT = Redis.new(host: 'localhost', port: 6379, db: 1)
# config/initializers/leaderboard.rb
redis_options = {:host => 'localhost', :port => 6379, :db => 1}
EVENT_VISITS = Leaderboard.new('event_visits', Leaderboard::DEFAULT_OPTIONS, redis_options)
现在我们可以从控制器的 show
操作中调用它
class EventsController < ApplicationController
def show
...
REDIS_CLIENT.zincrby('events_visits', 1, @event.id)
# or
EVENT_VISITS.change_score_for(@event.id, 1)
end
end
# data in Redis
{"db":1,"key":"events_visits","ttl":-1,"type":"zset","value":[["1",1.0],...,["2",4.0],["7",22.0]],...}
当我们有数百万个排序集成员时,向排序集添加项目最终会变慢,但 Redis 对于大多数用例来说足够快。我们现在可以使用此排序集来确定每个活动的 rank
和 score
。或者我们可以使用 REDIS_CLIENT.zrange('events_visits', 0, 9)
显示前 10 个活动。
由于我们使用 Redis 来存储非常不同类型的数据(缓存、作业等),因此我们需要小心不要耗尽 RAM。Redis 会自行驱逐键,但它无法区分持有陈旧缓存的键与对我们的应用程序重要的东西。
更多信息
我希望这篇文章对在 Ruby on Rails 应用程序中出于各种目的使用 Redis 的介绍很有用。如果您想了解更多信息,请查阅以下链接
2 条评论