我希望找到有关Rails 4中的缓存机制如何防止多个用户同时尝试重新生成缓存密钥的信息,即缓存踩踏:http://en.wikipedia.org/wiki/Cache_stampede
我无法通过谷歌搜索找到更多信息。如果我查看其他系统(例如Drupal),则通过数据库中的semaphores
表实现缓存标记预防。
答案 0 :(得分:6)
Rails没有内置机制来阻止缓存标记。
根据atomic_mem_cache_store
的自述文件(替代ActiveSupport::Cache::MemCacheStore
减轻缓存踩踏事件):
Rails(以及依赖于活动支持缓存存储的任何框架)都可以 不提供任何内置的解决方案
不幸的是,我猜这个宝石也无法解决你的问题。它支持片段缓存,但它仅适用于基于时间的过期。
在这里阅读更多相关信息: https://github.com/nel/atomic_mem_cache_store
我想到了这一点,并提出了我认为合理的解决方案。我没有证实这是有效的,并且可能有更好的方法来做到这一点,但我试图想到可以缓解大部分问题的最小变化。
我假设您在模板中执行类似cache model do
的操作,如DHH所述(http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)。问题是,当模型的updated_at
列发生更改时,cache_key
同样会发生更改,并且所有服务器都会尝试同时重新创建模板。为了防止服务器加盖,您需要在短时间内保留旧的cache_key。
您可以通过(dum da dum)缓存对象的cache_key,并使用短暂的到期时间(例如,1秒)和race_condition_ttl
。
您可以创建这样的模块并将其包含在您的模型中:
module StampedeAvoider
def cache_key
orig_cache_key = super
Rails.cache.fetch("/cache-keys/#{self.class.table_name}/#{self.id}", expires_in: 1, race_condition_ttl: 2) { orig_cache_key }
end
end
让我们回顾一下会发生什么。有许多服务器正在调用cache model
。如果您的模型包含StampedeAvoider
,则其cache_key
现在将提取/cache-keys/models/1
,并返回/models/1-111
(其中111是时间戳)等内容,cache
将用于获取已编译的模板片段。
更新模型后,model.cache_key
将开始返回/models/1-222
(假设222是新的时间戳),但在此之后的第一秒,cache
将继续/models/1-111
因为这是cache_key
返回的内容。一旦通过,所有服务器将在/cache-keys/models/1
上获得缓存未命中并将尝试重新生成它。如果它们都立即重新创建它,它将击败覆盖cache_key
的点。但是因为我们将race_condition_ttl
设置为2,所以除了第一个服务器之外的所有服务器都将延迟2秒,在此期间它们将继续根据旧的缓存密钥获取旧的缓存模板。一旦2秒过去,fetch
将开始返回新的缓存密钥(它将由尝试读取/更新/cache-keys/models/1
的第一个线程更新)并且它们将获得缓存命中,返回由第一个线程编译的模板。
钽哒!踩踏踩踏。
请注意,如果你这样做,你会做两倍的缓存读取,但是根据常见的踩踏事件,它可能是值得的。
我没有测试过这个。如果你试试,请告诉我它是怎么回事:))
答案 1 :(得分:5)
:race_condition_ttl
中的ActiveSupport::Cache::Store#fetch
设置应有助于避免此问题。正如documentation所说:
设置:race_condition_ttl在非常频繁使用缓存条目且负载很重的情况下非常有用。如果缓存过期并且由于负载过重,七个不同的进程将尝试本机读取数据,然后它们都会尝试写入缓存。为避免这种情况,查找过期缓存条目的第一个进程将使缓存过期时间超过:race_condition_ttl中设置的值。是的,此过程将陈旧值的时间延长了几秒钟。由于前一个缓存的使用寿命延长,其他进程将继续使用稍微过时的数据。与此同时,第一个进程将继续,并将写入缓存新值。之后,所有流程都将开始获得新价值。关键是保持:race_condition_ttl小。
答案 2 :(得分:0)
好问题。适用于单个多线程Rails服务器但不适用于多进程(或)环境的部分答案(感谢Nick Urban绘制此区别)是ActionView模板编译代码在每个模板的互斥锁上阻塞。见line 230 in template.rb here。请注意,在获取锁定之前和之后都会检查已完成的编译。
效果是序列化编译同一模板的尝试,其中只有第一个实际上会进行编译,其余的将获得已经完成的结果。
答案 3 :(得分:0)
非常有趣的问题。我搜索谷歌(如果你搜索“狗堆”而不是“踩踏”,你会得到更多的结果)但是像你一样,我没有得到任何答案,除了这篇博文:protecting from dogpile using memcache。
基本上它会将片段存储在两个键中:key:timestamp
(其中时间戳为活动记录对象的updated_at
)和key:last
。
def custom_write_dogpile(key, timestamp, fragment, options)
Rails.cache.write(key + ':' + timestamp.to_s, fragment)
Rails.cache.write(key + ':last', fragment)
Rails.cache.delete(key + ':refresh-thread')
fragment
end
现在,当从缓存中读取并尝试获取不存在的缓存时,它会尝试改为使用key:last
片段:
def custom_read_dogpile(key, timestamp, options)
result = Rails.cache.read(timestamp_key(name, timestamp))
if result.blank?
Rails.cache.write(name + ':refresh-thread', 0, raw: true, unless_exist: true, expires_in: 5.seconds)
if Rails.cache.increment(name + ':refresh-thread') == 1
# The cache didn't exists
result = nil
else
# Fetch the last cache, as the new one has not been created yet
result = Rails.cache.read(name + ':last')
end
end
result
end
这是我以前链接过的Moshe Bergman的简要摘要,或者你可以找到here。
答案 4 :(得分:0)
没有针对memcache踩踏事件的保护措施。当涉及多台机器并且在这些多台机器上有多个进程时,这是一个真正的问题。 -Ouch -
当其中一个关键进程“死亡”而任何“锁定”......被锁定时,问题就更加复杂了。
为了防止踩踏事件,您必须在数据到期之前重新计算数据。因此,如果您的数据有效期为10分钟,则需要在第5分钟再次重新生成,并在新的到期时间内重新设置数据10分钟。因此,您不要等到数据到期才能再次设置它。
也不应允许您的数据在10分钟后过期,但每5分钟重新计算一次,并且永远不会过期。 :)
你可以使用wget& cron定期调用代码。
我建议使用redis,它可以保存数据并在崩溃发生时重新加载。
-daniel
答案 5 :(得分:0)
合理的策略是:
:race_condition_ttl
至少刷新资源所需的预期时间。将它设置为比预期更少的时间进行刷新是不可取的,因为愤怒的暴徒最终会试图刷新它,导致踩踏事件。:expires_in
时间计算为最长可接受的到期时间 减去 :race_condition_ttl
,以允许单个工作人员刷新资源,避免踩踏事件。使用上述策略将确保您不会超过到期/过期期限并避免踩踏事件。它的工作原理是因为只有一个工作人员通过刷新,而愤怒的暴徒使用缓存值延迟,race_condition_ttl
延长时间直到最初预期的到期时间。