【问题标题】:Rails cache counterRails 缓存计数器
【发布时间】:2017-10-26 12:29:44
【问题描述】:

我有一个简单的 Ruby 方法来限制某些执行。

MAX_REQUESTS = 60
# per
TIME_WINDOW = 1.minute

def throttle
  cache_key = "#{request.ip}_count"
  count = Rails.cache.fetch(cache_key, expires_in: TIME_WINDOW.to_i) { 0 }

  if count.to_i >= MAX_REQUESTS
    render json: { message: 'Too many requests.' }, status: 429
    return
  end
  Rails.cache.increment(cache_key)
  true
end

经过一些测试,我发现cache_key 永远不会失效。

我通过binding.pry 进行了调查,发现了问题:

[35] pry(#<Refinery::ApiReferences::Admin::ApiHostsController>)> Rails.cache.write(cache_key, count += 1, expires_in: 60, raw: true)
=> true
[36] pry(#<Refinery::ApiReferences::Admin::ApiHostsController>)> Rails.cache.send(:read_entry, cache_key, {})
=> #<ActiveSupport::Cache::Entry:0x007fff1e34c978 @created_at=1495736935.0091069, @expires_in=60.0, @value=11>
[37] pry(#<Refinery::ApiReferences::Admin::ApiHostsController>)> Rails.cache.increment(cache_key)
=> 12
[38] pry(#<Refinery::ApiReferences::Admin::ApiHostsController>)> Rails.cache.send(:read_entry, cache_key, {})
=> #<ActiveSupport::Cache::Entry:0x007fff1ee105a8 @created_at=1495736965.540865, @expires_in=nil, @value=12>

所以,increment 正在清除 expires_in 值并更改 created_at,常规写入也会做同样的事情。

如何防止这种情况发生?我只想更新给定缓存键的

更新

根据我尝试过的建议:

MAX_REQUESTS = 60
# per
TIME_WINDOW = 1.minute

def throttle
  cache_key = "#{request.ip}_count"
  count = Rails.cache.fetch(cache_key, expires_in: TIME_WINDOW.to_i, raw: true) { 0 }

  if count.to_i >= MAX_REQUESTS
    render json: { message: 'Too many requests.' }, status: 429
    return
  end
  Rails.cache.increment(cache_key)
  true
end

没有效果。缓存不会过期。

【问题讨论】:

    标签: ruby-on-rails ruby caching


    【解决方案1】:

    这是一个“解决方案”,我不会将其标记为正确,因为这肯定没有必要?

    MAX_REQUESTS = 60
    # per
    TIME_WINDOW = 1.minute
    
    def throttle
      count_cache_key = "#{request.ip}_count"
      window_cache_key = "#{request.ip}_window"
    
      window = Rails.cache.fetch(window_cache_key) { (Time.zone.now + TIME_WINDOW).to_i }
    
      if Time.zone.now.to_i >= window
        Rails.cache.write(window_cache_key, (Time.zone.now + TIME_WINDOW).to_i)
        Rails.cache.write(count_cache_key, 1)
      end
    
      count = Rails.cache.read(count_cache_key) || 0
    
      if count.to_i >= MAX_REQUESTS
        render json: { message: 'Too many requests.' }, status: 429
        return
      end
    
      Rails.cache.write(count_cache_key, count + 1)
      true
    end
    

    【讨论】:

      【解决方案2】:

      在 Rails 缓存中增加 raw(使用 raw: true 选项)完全符合您的要求,即它仅更新值,不是过期时间。但是,在调试时,您不能非常依赖read_entry 的输出,因为这与存储在缓存中的原始值不完全对应,因为缓存存储在仅存储原始值时不会返回到期时间。

      这就是为什么通常情况下(没有raw)选项,Rails 不只存储原始值,而是创建一个cache Entry object,除了值之外,它还保存其他数据,例如到期时间。然后它序列化这个对象并将其保存到缓存存储中。在读回该值后,它会反序列化对象并仍然可以访问所有信息,包括到期时间。

      但是,由于您无法递增序列化对象,因此您需要存储一个 原始值,即使用 raw: true 选项。这使得 Rails 直接存储值并将到期时间作为参数传递给缓存存储 write 方法(不可能从存储中读回它)。

      因此,总而言之,在缓存递增值时必须使用raw: true,并且到期时间通常会保存在缓存存储中。请参阅以下测试(在 mem_cache_store 商店完成):

      # cache_test.rb
      cache_key = "key"
      
      puts "setting..."
      Rails.cache.fetch(cache_key, expires_in: 3.seconds, raw: true) { 1 }
      puts "#{Time.now} cached value: #{Rails.cache.read(cache_key)}"
      
      sleep(2)
      puts "#{Time.now} still cached: #{Rails.cache.read(cache_key)}"
      
      puts "#{Time.now} incrementing..."
      Rails.cache.increment(cache_key)
      puts "#{Time.now} incremented value: #{Rails.cache.read(cache_key)}"
      
      sleep(1)
      puts "#{Time.now} gone!: #{Rails.cache.read(cache_key).inspect}"
      

      运行这个,你会得到:

      $ rails runner cache_test.rb 
      Running via Spring preloader in process 31666
      setting...
      2017-05-25 22:15:26 +0200 cached value: 1
      2017-05-25 22:15:28 +0200 still cached: 1
      2017-05-25 22:15:28 +0200 incrementing...
      2017-05-25 22:15:28 +0200 incremented value: 2
      2017-05-25 22:15:29 +0200 gone!: nil
      

      如您所见,该值已在未重置到期时间的情况下增加。

      更新:我为您的代码设置了一个最小测试,虽然不是通过真正的控制器运行,但仅作为脚本运行。我只对您的 OP 中的 throttle 代码做了 4 处小改动:

      1. 降低了时间窗口
      2. render 更改为简单的puts
      3. 仅使用单个密钥,就好像请求来自单个 IP 地址一样
      4. 打印增量值

      脚本:

      # chache_test2.rb
      MAX_REQUESTS = 60
      # per
      #TIME_WINDOW = 1.minute
      TIME_WINDOW = 3.seconds
      
      def throttle
        #cache_key = "#{request.ip}_count"
        cache_key = "127.0.0.1_count"
        count = Rails.cache.fetch(cache_key, expires_in: TIME_WINDOW.to_i, raw: true) { 0 }
      
        if count.to_i >= MAX_REQUESTS
          #render json: { message: 'Too many requests.' }, status: 429
          puts "too many requests"
          return
        end
      
        puts Rails.cache.increment(cache_key)
        true
      end
      
      62.times do |i|
        throttle
      end
      
      sleep(3)
      throttle
      

      运行打印以下内容:

      $ rails runner cache_test2.rb 
      Running via Spring preloader in process 32589
      2017-05-26 06:11:26 +0200 1
      2017-05-26 06:11:26 +0200 2
      2017-05-26 06:11:26 +0200 3
      2017-05-26 06:11:26 +0200 4
      ...
      2017-05-26 06:11:26 +0200 58
      2017-05-26 06:11:26 +0200 59
      2017-05-26 06:11:26 +0200 60
      2017-05-26 06:11:26 +0200 too many requests
      2017-05-26 06:11:26 +0200 too many requests
      2017-05-26 06:11:29 +0200 1
      

      也许您根本没有在开发中配置缓存?我建议在 memcached 存储中进行测试,这是生产环境中最受青睐的缓存存储。在开发中,需要显式开启:

      # config/environemnts/development.rb
      config.cache_store = :mem_cache_store
      

      另外,如果您运行的是最新的 Rails 5.x 版本,您可能需要运行 rails dev:cache 命令 which creates 开发配置中使用的 tmp/caching-dev.txt 文件才能在开发环境中实际启用缓存。

      【讨论】:

      • 感谢您的回复!试过了(见上文)没有用。我错过了什么吗?
      • 它适用于我,请参阅更新的答案。您是否尝试过上面的琐碎测试?您使用的是什么缓存存储?它可能不会在所有缓存存储中以相同的方式工作。我使用 Dalli gem 在 memcached 上测试了所有这些。
      • 嗯,尝试了您提供的代码,在我项目的 rails runner 下运行它,同样的问题,60 岁后永远“请求太多”。我确定我的 cache_store:memory_store。接下来,我将尝试在我的项目中设置dalli:mem_cache_store。 (Rails 4.2.7.1 供参考。)
      猜你喜欢
      • 1970-01-01
      • 2019-01-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-21
      • 2020-03-07
      相关资源
      最近更新 更多