Лучший способ объединить кэширование фрагментов и объектов для memcached и Rails

StackOverflow https://stackoverflow.com/questions/1034910

Вопрос

Допустим, у вас есть фрагмент страницы, на котором отображаются самые последние сообщения, и срок его действия истекает через 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", было бы лучше сохранить это в одном месте.

Идеи?

Это было полезно?

Решение

У Эвана Уивера Плагин блокировки решает эту проблему.

Вы также можете легко реализовать что-то подобное самостоятельно, если вам нужно другое поведение, например, более мелкозернистое управление.Основная идея состоит в том, чтобы обернуть код вашего контроллера в блок, который фактически выполняется только в том случае, если представлению нужны эти данные:

# 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 непосредственно в представлении:

  • сохраняет код finder там, где разработчики ожидают его по соглашению
  • сохраняет представление в неведении относительно имени модели / метода, позволяя больше повторно использовать представление

Я думаю, что cache_fu может иметь аналогичную функциональность в одной из своих версий / форков, но не могу вспомнить конкретно.

Преимущество, которое вы получаете от 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 %>

Для наглядности вы также могли бы рассмотреть возможность переноса рендеринга недавнего поста в его собственный частичный:

<% 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 все еще стоит на повестке дня, требуя названия ключа в двух местах.Обычно я делаю это так, как предложил Ларс, но на самом деле все зависит от вкуса.Другие разработчики, которых я знаю, придерживаются проверки fragment, существуют.

Обновить:

Если вы посмотрите на документы fragment, вы можете увидеть, как он избавляется от необходимости использования префикса view:

# 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

Ларс делает действительно хорошее замечание о том, что существует небольшая вероятность неудачи при использовании:

unless fragment_exist?("recent_posts")

потому что существует разрыв между тем, когда вы проверяете кэш, и тем, когда вы используете кэш.

Плагин, о котором упоминает Джейсон (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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top