¿Cómo funciona el almacenamiento en caché basado en claves?

10

Hace poco leí un artículo en el blog 37Signals y me pregunto cómo es que obtienen la clave de caché.

Está muy bien tener una clave de caché que incluya la marca de tiempo del objeto (esto significa que cuando actualice el objeto, la caché se invalidará); pero, ¿cómo se usa la clave de caché en una plantilla sin causar un golpe de base de datos para el objeto que está tratando de recuperar de la caché?

Específicamente, ¿cómo afecta esto a las relaciones de uno a muchos, por ejemplo, cuando representa los comentarios de una publicación?

Ejemplo en Django:

{% for comment in post.comments.all %}
   {% cache comment.pk comment.modified %}
     <p>{{ post.body }}</p>
   {% endcache %}
{% endfor %}

El almacenamiento en caché en Rails es diferente a las solicitudes de memcached, por ejemplo (sé que convierten su clave de caché en algo diferente). ¿También guardan en caché la clave de caché?

Dominic Santos
fuente
¡Eche un vistazo a rossp.org/blog/2012/feb/29/fragment-caching para ver un ejemplo de Django!
vdboor
Ya he echado un vistazo a eso y parece sufrir exactamente el mismo problema. Los datos que está tratando de almacenar en caché son necesarios para acceder al caché. Lo único en lo que parece estar ahorrando es en la costosa operación interna que es diferente a la mayoría de los casos de uso para este tipo de almacenamiento en caché.
Dominic Santos
Eso es cierto, y también sucede con el código de 37 señales, se centra en el código de representación. El truco es almacenar en caché toda la lista en otro contenedor también, o almacenar en caché la recuperación del objeto en otro lugar.
vdboor
En realidad, su estrategia de almacenamiento en caché parece un poco más educada. También recomiendo este artículo: 37signals.com/svn/posts/…
JensG
Parece que su fragmento de código tiene un error tipográfico, ¿estaba post.bodydestinado a ser comment.body?
Izkata

Respuestas:

3

Para almacenar en caché un volcado directo de un solo objeto ya cargado, sí, no gana nada o casi nada. Eso no es lo que esos ejemplos están describiendo: están describiendo una jerarquía, donde cualquier cambio a algo más bajo también debería desencadenar una actualización de todo lo que está más arriba en la jerarquía.

El primer ejemplo, del blog 37signals, se usa Project -> Todolist -> Todocomo jerarquía. Un ejemplo poblado podría verse así:

Project: Foo (last_modified: 2014-05-10)
   Todolist:  Bar1 (last_modified: 2014-05-10)
       Todo:  Bang1 (last_modified: 2014-05-09)
       Todo:  Bang2 (last_modified: 2014-05-09)

   Todolist:  Bar2 (last_modified: 2014-04-01)
       Todo:  Bang3 (last_modified: 2014-04-01)
       Todo:  Bang4 (last_modified: 2014-04-01)

Entonces, digamos que Bang3se actualizó. Todos sus padres también se actualizan:

Project: Foo (last_modified: 2014-05-16)
   Todolist:  Bar2 (last_modified: 2014-05-16)
       Todo:  Bang3 (last_modified: 2014-05-16)

Luego, cuando llega el momento de renderizar, la carga Projectdesde la base de datos es básicamente inevitable. Necesitas un punto para comenzar. Sin embargo, debido a que last_modifiedes un indicador de todos sus elementos secundarios , eso es lo que utiliza como clave de caché antes de intentar cargar los elementos secundarios.


Si bien las publicaciones del blog usan plantillas separadas, las agruparé en una sola. Con suerte, ver la interacción completa en un lugar lo hará un poco más claro.

Entonces, la plantilla de Django podría verse así:

{% cache 9999 project project.cache_key %}
<h2>{{ project.name }}<h2>
<div>
   {% for list in project.todolist.all %}
   {% cache 9999 todolist list.cache_key %}
      <ul>
         {% for todo in list.todos.all %}
            <li>{{ todo.body }}</li>
         {% endfor %}
      </ul>
   {% endcache %}
   {% endfor %}
</div>
{% endcache %}

Digamos que pasamos un proyecto cuyo cache_keytodavía existe en el caché. Debido a que propagamos los cambios a todos los objetos relacionados al padre, el hecho de que esa clave en particular aún exista significa que todo el contenido renderizado se puede extraer de la memoria caché.

Si ese Proyecto en particular acaba de actualizarse, por ejemplo, como en el caso Fooanterior, tendrá que representar a sus hijos, y solo entonces ejecutará la consulta para todos los Todolistas para ese Proyecto. Del mismo modo para un Todolist específico: si la clave_caché de esa lista existe, entonces los todos dentro de ella no han cambiado, y todo se puede extraer de la caché.

Observe también cómo no estoy usando todo.cache_keyen esta plantilla. No vale la pena, ya que como dices en la pregunta, bodyya se ha retirado de la base de datos. Sin embargo, los accesos a la base de datos no son la única razón por la que puede almacenar algo en caché. Por ejemplo, tomar texto de marcado sin formato (como lo que escribimos en los cuadros de preguntas / respuestas en StackExchange) y convertirlo a HTML puede llevar suficiente tiempo para que el resultado del almacenamiento en caché sea más eficiente.

Si así fuera, el bucle interno de la plantilla podría verse más así:

         {% for todo in list.todos.all %}
            {% cache 9999 todo todo.cache_key %}
               <li>{{ todo.body|expensive_markup_parser }}</li>
            {% endcache %}
         {% endfor %}

Entonces, para reunir todo, volvamos a mis datos originales en la parte superior de esta respuesta. Si suponemos:

  • Todos los objetos habían sido almacenados en caché en su estado original.
  • Bang3 acaba de actualizarse
  • Estamos renderizando la plantilla modificada (incluida expensive_markup_parser)

Entonces así es como se cargaría todo:

  • Foo se recupera de la base de datos
  • Foo.cache_key (2014-05-16) no existe en el caché
  • Foo.todolists.all()se consulta: Bar1y Bar2se recuperan de la base de datos
  • Bar1.cache_key(2014-05-10) ya existe en el caché ; recuperarlo y sacarlo
  • Bar2.cache_key (2014-05-16) no existe en el caché
  • Bar2.todos.all()se consulta: Bang3y Bang4se recuperan de la base de datos
  • Bang3.cache_key (2014-05-16) no existe en el caché
  • {{ Bang3.body|expensive_markup_parser }} se representa
  • Bang4.cache_key(2014-04-01) ya existe en el caché ; recuperarlo y sacarlo

Los ahorros del caché en este pequeño ejemplo son:

  • Se evitó el impacto en la base de datos: Bar1.todos.all()
  • expensive_markup_parserevitadas 3 veces: Bang1, Bang2yBang4

Y, por supuesto, la próxima vez que Foo.cache_keyse vea , se encontrará, por lo que el único costo para renderizar es recuperar Foosolo de la base de datos y consultar el caché.

Izkata
fuente
-2

Su ejemplo es bueno si necesita un poco de recuperación o procesamiento de datos para cada comentario. Si solo toma el cuerpo y lo muestra, el caché será inútil. Pero puede almacenar en caché todos los árboles de comentarios (incluido {% para%}). En este caso, debe invalidarlo con cada comentario agregado, para que pueda colocar la marca de tiempo del último comentario o el recuento de comentarios en algún lugar en Publicar y construir una clave de caché de comentarios con él. Si prefiere datos más normalizados y usa comentarios en una sola página, puede borrar una clave de caché al guardar comentarios.

Para mí, guardar el recuento de comentarios en la publicación se ve lo suficientemente bueno (si no permite eliminar y editar comentarios): tiene un valor para mostrar en cualquier lugar con la publicación y una clave para el almacenamiento en caché.

ilvar
fuente