Russian Doll Caching e os Trade-offs do Cache Store

Published on 2026-08-17·v1.0

Objective

Russian Doll caching é a estratégia de expiração baseada em chave do Rails para cache de fragmentos: em vez de expirar manualmente uma entrada de cache quando os dados subjacentes mudam (observers, sweepers, chamadas explícitas a expire_fragment), toda chave de cache embute informação suficiente (a classe do registro, id, e updated_at) para que uma mudança nos dados produza automaticamente uma chave totalmente nova. A entrada antiga nunca é explicitamente deletada; ela simplesmente se torna inalcançável e eventualmente é removida pela própria política LRU do cache store. Isso só funciona de ponta a ponta se todo fragmento, incluindo os que envolvem coleções, incluir os dados certos na sua própria chave, e só tem boa performance se o cache store subjacente for de fato rápido, o que não é automático.

Use Cases

  • Fazer cache de um fragmento de view para um único registro (um comentário, uma página de produto) para que nunca precise de invalidação manual quando o registro é editado.
  • Fazer cache de um fragmento que envolve uma coleção de registros (uma lista de comentários, uma grade de produtos) onde qualquer item mudando deveria invalidar o fragmento externo também.
  • Escolher um cache store (MemoryStore, FileStore, Dalli/ redis-cache-store) com base em latência de fato medida, não em qual é o padrão do Rails.
  • Reconhecer quando fazer cache de um fragmento é de fato mais lento do que não fazer cache nenhum, para conteúdo barato de renderizar e apoiado por um cache store remoto.

Deep Dive

O mecanismo de expiração baseado em chave

plaintext
<% cache comment do %> <%= comment.body %> <% end %>

cache comment constrói uma chave que embute o nome da classe, o id, e o updated_at de comment (mais um digest do próprio template). Editar o comentário muda seu updated_at, que muda a chave, o que significa que o fragmento de view é renderizado de novo e armazenado sob uma chave nova; a entrada antiga simplesmente nunca é buscada de novo. Nenhum código explícito de invalidação é jamais escrito.

Fragmentos aninhados precisam que a chave externa saiba sobre a coleção interna

plaintext
<% cache product do %> <% cache [product, product.reviews.maximum(:updated_at)] do %> <% product.reviews.each do |review| %> <% cache review do %> <%= review.body %> <% end %> <% end %> <% end %> <% end %>

Se a chave do fragmento externo de product dependesse só do próprio updated_at de product, editar uma única review não mudaria o updated_at do produto; então o fragmento externo continuaria servindo uma página cacheada obsoleta mesmo com o fragmento interno de review, corretamente chaveado, tendo se atualizado sozinho. Incluir product.reviews.maximum(:updated_at) na chave externa faz qualquer edição de review mudar a chave externa também.

touch: true propaga a invalidação pela cadeia de associação de graça

plaintext
class Review < ApplicationRecord belongs_to :product, touch: true end

Salvar uma Review agora também atualiza automaticamente product.updated_at. Qualquer fragmento cacheado com cache product pega essa chave nova sem a ginástica de chave aninhada acima; touch: true é a correção mais elegante exatamente para esse problema de propagação de invalidação, onde quer que a associação permita.

A latência do cache store domina a decisão

Em ordem de magnitude, operações fetch por segundo, do mais rápido ao mais lento: LruRedux (em processo) > MemoryStore > FileStore > Dalli/Redis local > um Redis/Memcache remoto pela rede, que pode ser aproximadamente quatro ordens de magnitude mais lento que um cache em processo. Um fragmento que custa alguns milissegundos para renderizar pode renderizar mais rápido sem cache do que custa a ida e volta até um cache store remoto; cache só compensa quando o custo de renderização de fato excede a própria latência do cache store.

Trade-offs

  • FileStore (o padrão do Rails em algumas configurações) não é LRU: ele expira entradas por horário de escrita, não por frequência de acesso, então uma chave frequentemente acessada pode ser removida antes de uma chave que ninguém mais lê. Também tem performance ruim em plataformas com sistemas de arquivos efêmeros ou em rede.
  • Um cache store remoto pode tornar um fragmento barato de renderizar mais lento do que nenhum cache: antes de envolver algo em cache do...end, vale a pena saber (medindo, não assumindo) tanto o custo de renderização quanto a latência real de ida e volta do cache store.
  • Russian Doll caching aninhado adiciona overhead real de autoria: todo nível que envolve uma coleção precisa ter sua chave deliberadamente estendida com o updated_at máximo daquela coleção (ou a associação precisa de touch: true); pular isso em apenas um nível aninhado reintroduz silenciosamente conteúdo obsoleto naquele nível, sem nenhum erro para sinalizar que isso aconteceu.

Documentation Links