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

时间:2009-06-23 20:16:22

标签: ruby-on-rails ruby caching memcached fragment

假设您有一个显示最新帖子的页面片段,您将在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”的重复,最好将其保存在一个地方。

想法?

5 个答案:

答案 0 :(得分:12)

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。

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

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

我认为cache_fu可能在其中一个版本/分叉中具有类似功能,但无法具体回忆。

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

答案 1 :(得分:3)

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

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

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

答案 2 :(得分:1)

您可能还想查看

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

答案 3 :(得分:1)

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

unless fragment_exist?("recent_posts")

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

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

答案 4 :(得分:1)

就像一个想法:

在应用程序控制器中定义

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