为 memcached 和 Rails 组合片段和对象缓存的最佳方式
Posted
技术标签:
【中文标题】为 memcached 和 Rails 组合片段和对象缓存的最佳方式【英文标题】:Best way to combine fragment and object caching for memcached and Rails 【发布时间】:2010-11-05 07:34:09 【问题描述】:假设您有一个显示最新帖子的页面片段,您将在 30 分钟后将其过期。我在这里使用 Rails。
<% cache("recent_posts", :expires_in => 30.minutes) do %>
...
<% end %>
如果片段存在,显然您不需要进行数据库查找来获取最新帖子,因此您也应该能够避免这种开销。
我现在在控制器中做的是这样的事情,似乎可以工作:
unless Rails.cache.exist? "views/recent_posts"
@posts = Post.find(:all, :limit=>20, :order=>"updated_at DESC")
end
这是最好的方法吗?安全吗?
我不明白的一件事是为什么片段的密钥是“recent_posts
”,稍后检查时是“views/recent_posts
”,但我在看了memcached -vv
后想出了这个,看看它在使用什么.另外,我不喜欢手动输入“recent_posts
”的重复,最好把它放在一个地方。
想法?
【问题讨论】:
【参考方案1】:Evan Weaver 的Interlock Plugin 解决了这个问题。
如果您需要不同的行为,例如更细粒度的控制,您也可以自己轻松实现类似的功能。基本思想是将您的控制器代码包装在一个块中,该块仅在视图需要该数据时才实际执行:
# in FooController#show
@foo_finder = lambda Foo.find_slow_stuff
# in foo/show.html.erb
cache 'foo_slow_stuff' do
@foo_finder.call.each do
...
end
end
如果您熟悉 ruby 元编程的基础知识,那么很容易将其封装在您喜欢的更简洁的 API 中。
这比将查找器代码直接放在视图中要好:
按照惯例将查找器代码保留在开发人员期望的位置 保持视图不知道模型名称/方法,允许更多视图重用我认为 cache_fu 可能在它的一个版本/分支中具有类似的功能,但无法具体回忆。
您从 memcached 获得的优势与您的缓存命中率直接相关。注意不要通过多次缓存相同的内容来浪费您的缓存容量并导致不必要的丢失。例如,不要同时缓存一组记录对象及其 html 片段。通常,片段缓存会提供最佳性能,但这实际上取决于您的应用程序的具体情况。
【讨论】:
虽然我认为 Lar 的答案可能更清楚一点,并且避免使用元编程,但您的答案更好,因为广告更接近 MVC。我已将它们两个都视为有效,但将由社区决定(不会选择一个被接受:) 谢谢! 我同意,并且发现这也是一个不错的解决方案。至于重用视图,是的,总的来说这是一个很好的原则。您必须让控制器也提供缓存键才能在这里工作。顺便说一句,有趣的是,关于片段缓存的 API 文档在第一个示例中违反了 MVC 原则:api.rubyonrails.org/classes/ActionController/Caching/…【参考方案2】:如果缓存在您在控制器中检查它的时间之间过期会发生什么 以及在视图渲染中检查的时间?
我会在模型中创建一个新方法:
class Post
def self.recent(count)
find(:all, :limit=> count, :order=>"updated_at DESC")
end
end
然后在视图中使用它:
<% cache("recent_posts", :expires_in => 30.minutes) do %>
<% Post.recent(20).each do |post| %>
...
<% end %>
<% end %>
为清楚起见,您还可以考虑将最近发布的帖子的渲染移至其自己的部分:
<% cache("recent_posts", :expires_in => 30.minutes) do %>
<%= render :partial => "recent_post", :collection => Post.recent(20) %>
<% end %>
【讨论】:
我认为您的解决方案是迄今为止最好的解决方案,尽管从技术上讲这可能违反 MVC,但很明显。 Jason Watkin 的回答是一个有效的选择。【参考方案3】:你可能还想看看
Fragment Cache Docs
这允许你这样做:
<% cache("recent_posts", :expires_in => 30.minutes) do %>
...
<% end %>
控制器
unless fragment_exist?("recent_posts")
@posts = Post.find(:all, :limit=>20, :order=>"updated_at DESC")
end
尽管我承认 DRY 的问题仍然需要在两个地方提供密钥的名称。我通常这样做类似于 Lars 的建议,但这真的取决于口味。我认识的其他开发人员坚持检查片段存在。
更新:
如果你查看片段文档,你可以看到它是如何摆脱需要视图前缀的:
# File vendor/rails/actionpack/lib/action_controller/caching/fragments.rb, line 33
def fragment_cache_key(key)
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
end
【讨论】:
啊,有道理。片段存在?方法可能不需要 cache_key 上的“view/”前缀?【参考方案4】:Lars 提出了一个非常好的观点,即使用:
unless fragment_exist?("recent_posts")
因为在检查缓存和使用缓存之间存在间隙。
jason 提到的插件 (Interlock) 非常优雅地处理了这个问题,它假设如果您正在检查片段是否存在,那么您可能还会使用片段并因此在本地缓存内容。我使用 Interlock 正是出于这些原因。
【讨论】:
【参考方案5】:只是一个想法:
在应用程序控制器中定义
def when_fragment_expired( name, time_options = nil )
# idea of avoiding race conditions
# downside: needs 2 cache lookups
# in view we actually cache indefinetely
# but we expire with a 2nd fragment in the controller which is expired time based
return if ActionController::Base.cache_store.exist?( 'fragments/' + name ) && ActionController::Base.cache_store.exist?( fragment_cache_key( name ) )
# the time_fraqgment_cache uses different time options
time_options = time_options - Time.now if time_options.is_a?( Time )
# set an artificial fragment which expires after given time
ActionController::Base.cache_store.write("fragments/" + name, 1, :expires_in => time_options )
ActionController::Base.cache_store.delete( "views/"+name )
yield
end
然后在任何动作中使用
def index
when_fragment_expired "cache_key", 5.minutes
@object = YourObject.expensive_operations
end
end
在视图中
cache "cache_key" do
view_code
end
【讨论】:
以上是关于为 memcached 和 Rails 组合片段和对象缓存的最佳方式的主要内容,如果未能解决你的问题,请参考以下文章