我想找到有关Rails 4缓存机制如何防止多个用户同时尝试重新生成缓存密钥的信息,即缓存失效的解决方案:http://en.wikipedia.org/wiki/Cache_stampede
通过谷歌搜索,我没有找到太多相关信息。如果看其他系统(例如Drupal)的话,缓存失效的预防是通过数据库中的semaphores
表来实现的。
我想找到有关Rails 4缓存机制如何防止多个用户同时尝试重新生成缓存密钥的信息,即缓存失效的解决方案:http://en.wikipedia.org/wiki/Cache_stampede
通过谷歌搜索,我没有找到太多相关信息。如果看其他系统(例如Drupal)的话,缓存失效的预防是通过数据库中的semaphores
表来实现的。
atomic_mem_cache_store
的README(这是一个替代ActiveSupport::Cache::MemCacheStore
且减轻了缓存流行的缓存机制):cache model do
(http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)。问题是,当模型的updated_at
列更改时,cache_key
也会更改,所有服务器都会尝试同时重新创建模板。为了防止服务器发生拥堵,您需要保留旧的cache_key一段时间。race_condition_ttl
缓存对象的cache_key来实现这一点。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
model.cache_key
将开始返回/models/1-222
(假设222是新的时间戳),但在此之后的第一秒钟,cache
仍将看到/models/1-111
,因为它是由cache_key
返回的内容。一旦过去了1秒,所有服务器都会在/cache-keys/models/1
上获得缓存未命中,并尝试重新生成它。如果它们都立即重新创建,那么就会破坏覆盖cache_key
的目的。但是因为我们将race_condition_ttl
设置为2,除第一个服务器外,所有服务器都会延迟2秒钟,这段时间内它们将继续根据旧的缓存键获取旧的缓存模板。一旦过去了2秒钟,fetch
将开始返回新的缓存键(这将由试图读取/更新/cache-keys/models/1
的第一个线程更新),它们将获得缓存命中,返回由该第一个线程编译的模板。
哒哒!避免了奔跑的情况。
请注意,如果您这样做,将会读取缓存两次,但根据挤压的常见程度,这可能是值得的。:race_condition_ttl
设置在ActiveSupport::Cache::Store#fetch
中,有助于避免这个问题。正如文档所述:
在缓存条目非常频繁地使用并且负载很重的情况下,设置:race_condition_ttl非常有用。如果一个缓存过期,并且由于负载过重,七个不同的进程将尝试本地读取数据,然后它们都将尝试写入缓存。为了避免这种情况,发现过期缓存条目的第一个进程将通过:race_condition_ttl设置来增加缓存过期时间。是的,这个进程正在将陈旧值的时间延长几秒钟。由于上一个缓存的寿命延长了,其他进程将继续使用略微陈旧的数据更长一点时间。同时,该第一个进程将继续向缓存中写入新值。此后,所有进程都将开始获取新值。关键是保持:race_condition_ttl小。
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
好问题。适用于单个多线程Rails服务器但不适用于多进程环境(感谢Nick Urban提出这一区别)的部分答案是ActionView模板编译代码会阻塞在每个模板上的互斥锁上。请参见此处template.rb第230行。请注意,在获取锁之前和之后都会检查是否已完成编译。
其效果是序列化尝试编译相同模板的操作,其中只有第一个操作实际上会进行编译,其余操作将获得已经完成的结果。
Mutex.synchronize
只能在同一个进程内同步线程,无法解决进程间(或服务器间)的同步问题。 - Nick UrbanMemcache的stampede问题是无法避免的。当多台机器和多个进程参与时,这是一个真正的问题。-痛苦-
当其中一个关键进程“死亡”并且任何“锁定”都被锁定时,问题会变得更加复杂。
为了防止stampede,您必须在数据过期之前重新计算数据。因此,如果您的数据有效期为10分钟,则需要在第5分钟重新生成数据,并使用新的过期时间重新设置数据,以便不必等到数据过期才重新设置它。
还应该不允许数据在10分钟标记处过期,而应该每5分钟重新计算一次,这样它就永远不会过期。 :)
您可以使用wget和cron定期调用代码。
我建议使用redis,它将允许您在崩溃时保存数据并重新加载数据。
-丹尼尔
一个合理的策略是:
:race_condition_ttl
,时间至少要预计刷新资源所需的时间。将其设置为比刷新所需时间更短的时间是不可取的,因为愤怒的群众最终会试图刷新它,导致踩踏事件。:expires_in
时间,计算出最大可接受的到期时间减去:race_condition_ttl
,以允许单个工作人员刷新资源并避免踩踏事件。使用上述策略将确保您不会超过到期/陈旧截止日期,并避免踩踏事件。这有效的原因是只有一个工作人员可以通过刷新,而愤怒的群众则使用带有race_condition_ttl
扩展时间的缓存值一直保持在最初预定的到期时间。