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

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

Эван Уивер Модуль блокировки решает эту проблему.

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

# 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 может иметь аналогичную функциональность в одной из его версий /forks, но не может вспомнить конкретно.

Преимущество, которое вы получаете от memcached, напрямую связано со скоростью вашего кеша. Соблюдайте осторожность, чтобы не тратить время на кеш-память и вызывать ненужные промахи, кешируя один и тот же контент несколько раз. Например, не кэшируйте набор объектов записи, а также их фрагмент html в одно и то же время. Как правило, кэширование фрагментов будет обеспечивать лучшую производительность, но это действительно зависит от специфики вашего приложения.

Ответ 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 %>

Ответ 3

Вы также можете посмотреть

Документы кэш-памяти фрагментов

Что вы можете сделать:

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

Хотя я признаю, что проблема СУХОЙ все еще поднимает голову, требуя названия ключа в двух местах. Я обычно делаю это похоже на то, как предложил Ларс, но это действительно зависит от вкуса. Другие разработчики, которых я знаю, придерживаются проверенного фрагмента.

Update:

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

# 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

Ответ 4

Ларс очень хорошо говорит о наличии небольшого шанса на неудачу:

unless fragment_exist?("recent_posts")

потому что есть пробел между проверкой кеша и использованием кеша.

Плагин, который упоминает Джейсон (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