La mejor manera de combinar fragmentos y almacenamiento en caché de objetos para memcached y Rails
-
07-07-2019 - |
Pregunta
Digamos que tiene un fragmento de la página que muestra las publicaciones más recientes, y expira en 30 minutos. Estoy usando Rails aquí.
<% cache("recent_posts", :expires_in => 30.minutes) do %>
...
<% end %>
Obviamente, no necesita hacer la búsqueda en la base de datos para obtener las publicaciones más recientes si existe el fragmento, por lo que también debería poder evitar esa sobrecarga.
Lo que estoy haciendo ahora es algo así en el controlador que parece funcionar:
unless Rails.cache.exist? "views/recent_posts"
@posts = Post.find(:all, :limit=>20, :order=>"updated_at DESC")
end
¿Es esta la mejor manera? ¿Es seguro?
Una cosa que no entiendo es por qué la clave es " Recent_posts
" para el fragmento y " views / Recent_posts
" cuando lo comprobé más tarde, pero se me ocurrió esto después de ver memcached -vv
para ver qué estaba usando. Además, no me gusta la duplicación de ingresar manualmente " Recent_posts
" ;, sería mejor mantener eso en un solo lugar.
Ideas?
Solución
El Interlock Plugin de Evan Weaver resuelve este problema.
También puede implementar algo como esto fácilmente si necesita un comportamiento diferente, como un control más detallado. La idea básica es envolver el código de su controlador en un bloque que solo se ejecuta si la vista necesita esos datos:
# 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
Si está familiarizado con los conceptos básicos de la meta programación de ruby, es bastante fácil resumir esto en una API más limpia de su gusto.
Esto es superior a poner el código del buscador directamente en la vista:
- mantiene el código del buscador donde los desarrolladores lo esperan por convención
- mantiene la vista ignorante del nombre / método del modelo, permitiendo una mayor reutilización de la vista
Creo que cache_fu podría tener una funcionalidad similar en una de sus versiones / horquillas, pero no puedo recordar específicamente.
La ventaja que obtiene de memcached está directamente relacionada con su tasa de aciertos de caché. Tenga cuidado de no desperdiciar su capacidad de caché y causar errores innecesarios al almacenar en caché el mismo contenido varias veces. Por ejemplo, no guarde en caché un conjunto de objetos de registro, así como su fragmento html al mismo tiempo. En general, el almacenamiento en caché de fragmentos ofrecerá el mejor rendimiento, pero realmente depende de los detalles de su aplicación.
Otros consejos
¿Qué sucede si el caché caduca entre el momento en que lo verifica en el controlador? y el momento en que se verifica en el renderizado de la vista?
Crearía un nuevo método en el modelo:
class Post
def self.recent(count)
find(:all, :limit=> count, :order=>"updated_at DESC")
end
end
luego use eso en la vista:
<% cache("recent_posts", :expires_in => 30.minutes) do %>
<% Post.recent(20).each do |post| %>
...
<% end %>
<% end %>
Para mayor claridad, también podría considerar mover el renderizado de una publicación reciente a su propio parcial:
<% cache("recent_posts", :expires_in => 30.minutes) do %>
<%= render :partial => "recent_post", :collection => Post.recent(20) %>
<% end %>
También es posible que desee examinar
Que le permiten hacer esto:
<% cache("recent_posts", :expires_in => 30.minutes) do %>
...
<% end %>
Controlador
unless fragment_exist?("recent_posts")
@posts = Post.find(:all, :limit=>20, :order=>"updated_at DESC")
end
Aunque admito que el problema de DRY todavía se asoma y necesita el nombre de la clave en dos lugares. Por lo general, hago esto de manera similar a como Lars sugirió, pero realmente depende del gusto. Existen otros desarrolladores que conozco que siguen con la comprobación de fragmentos.
Actualización:
Si observa los fragmentos de documentos, puede ver cómo se deshace de necesitar el prefijo de vista:
# 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 señala muy bien que existe una pequeña posibilidad de falla al usar:
unless fragment_exist?("recent_posts")
porque hay una brecha entre cuando verifica el caché y cuando lo usa.
El complemento que menciona Jason (Interlock) maneja esto con mucha gracia al suponer que si está verificando la existencia del fragmento, entonces probablemente también usará el fragmento y, por lo tanto, almacenará en caché el contenido localmente. Uso Interlock por estas mismas razones.
solo como un pensamiento:
en el controlador de la aplicación define
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
luego, en cualquier acción, use
def index
when_fragment_expired "cache_key", 5.minutes
@object = YourObject.expensive_operations
end
end
a la vista
cache "cache_key" do
view_code
end