为memcached和Rails组合片段和对象缓存的最佳方法

假设您有一个显示最新post的页面片段,您将在30分钟后过期。 我在这里使用Rails。

 30.minutes) do %> ...  

显然,如果片段存在,您不需要进行数据库查找以获取最新的post,因此您也应该能够避免这种开销。

我现在正在做的是在控制器中这样的东西似乎工作:

 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以查看它的views/recent_posts之后我想出了这个。 另外,我不喜欢手动输入“ recent_posts ”的重复,最好将它保存在一个地方。

想法?

Evan Weaver的Interlock Plugin解决了这个问题。

如果您需要不同的行为,例如更精细的控制,您也可以轻松地自己实现这样的function。 基本思路是将控制器代码包装在一个只在视图需要该数据时才实际执行的块中:

 # 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中。

这比将finder代码直接放在视图中要好:

  • 将发现者代码按照惯例保留在开发人员期望的位置
  • 使视图不知道模型名称/方法,允许更多的视图重用

我认为cache_fu可能在其中一个版本/分支中具有类似的function,但不能具体回忆。

从memcached获得的优势与缓存命中率直接相关。 注意不要浪费您的缓存容量,并通过多次缓存相同的内容而导致不必要的错过。 例如,不要同时缓存一组记录对象及其html片段。 通常,片段缓存将提供最佳性能,但它实际上取决于应用程序的细节。

如果缓存在控制器中检查它的时间与在视图渲染中检查它的时间之间到期会发生什么?

我在模型中创建了一个新方法:

  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 %> 

为清楚起见,您还可以考虑将最近post的呈现移动到其自己的部分中:

 <% cache("recent_posts", :expires_in => 30.minutes) do %> <%= render :partial => "recent_post", :collection => Post.recent(20) %> <% end %> 

您可能还想了解一下

片段缓存文档

这允许你这样做:

 <% 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 

Lars非常关注使用以下几点失败的可能性:

 unless fragment_exist?("recent_posts") 

因为在检查缓存和使用缓存之间存在差距。

jason提到的插件(Interlock)通过假设如果你正在检查片段是否存在而非常优雅地处理它,那么你可能也会使用片段,从而在本地缓存内容。 我出于这些原因使用Interlock。

就像一个想法:

在应用控制器中定义

 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