ruby redis

Ruby on Rails是用于快速构建业务线应用程序的有用框架。 但是随着我们应用程序的增长,我们面临着扩展挑战。 我们可以使用多种工具,但是向我们的应用程序添加不同的技术会增加复杂性。 本文探讨了如何使用Redis内存数据结构存储作为多功能工具来解决不同的问题。

首先,我们需要安装Redis,这可以通过brewapt-getdocker 。 而且,当然,我们需要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 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售出9张门票,总计127美元,事件2售出16张门票,总计211美元。

快取清除

如果我们在缓存数据后马上又卖出了另一张票怎么办? 该网站将显示缓存的内容,直到Redis使用TTL清除这些密钥为止。 在某些情况下,显示过时的内容可能还可以,但我们希望显示准确的当前数据。 这是使用最近更新的时间戳记的地方。 我们将指定一个touch: true从子模型( 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 Hashes存储有关执行了多少作业以及花费了多长时间的统计信息。

定期作业也存储在哈希中。 我们本可以使用普通的Linux cron来启动工作,但这会在我们的系统中引入单点故障。 使用Sidekiq-cron,日程表存储在Redis中,并且Sidekiq工作人员运行的任何服务器都可以执行该工作(该库确保仅一个工作人员可以在计划的时间抓取特定工作)。 Sidekiq还具有出色的UI,我们可以在其中查看各种统计信息,并暂停计划的作业或按需执行它们。

Redis作为数据库

最后一项业务要求是跟踪每个事件页面的访问量,以便我们确定其受欢迎程度。 为此,我们将使用排序集。 我们可以直接创建REDIS_CLIENT来调用本地Redis命令,也可以使用排行榜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 
     
     ] 
     
     ] ,... 
     
     }

当我们有数以百万计的Sorted Set成员时,将项目添加到Sorted Set中最终会减慢速度,但是Redis对于大多数用例来说速度很快。 现在,我们可以使用此排序集来确定每个事件的rankscore 。 或者,我们可以使用REDIS_CLIENT.zrange('events_visits', 0, 9)显示前10个事件。

由于我们使用Redis存储非常不同类型的数据(缓存,作业等),因此我们需要注意不要耗尽RAM。 Redis会自行移出密钥,但是它无法区分保持旧缓存的密钥与对我们的应用程序重要的密钥之间的区别。

更多信息

我希望本文是Ruby on Rails应用程序中出于各种目的使用Redis的有用入门。