fbpx
Cache-Aside Wins
Publicado em: sexta-feira, 2 de ago de 2024

O uso de caching em sistemas distribuídos tornou-se uma prática essencial para melhorar a performance e a escalabilidade de aplicações.

A escolha da estratégia de caching adequada é um desafio que muitos enfrentam.

Com diversas abordagens disponíveis, é fácil se perder entre as opções e acabar implementando uma solução que, apesar de parecer eficiente inicialmente, pode se tornar um ponto de falha ou degradação ao longo do tempo.

Hoje vamos explorar a estratégia de Cache-Aside, sua aplicação e benefícios em comparação com outros padrões de caching, como Write-Through, Write-Behind, Read-Through, Refresh-Ahead e Cache Stampede Prevention.

Argumentarei, com base na minha experiência e por que considero Cache-Aside a escolha mais segura e eficaz para a maioria dos casos de uso, proporcionando não apenas simplicidade e controle, mas também uma otimização eficiente dos recursos de cache.

Contexto

No contexto de software modernos, a demanda por respostas rápidas e escalabilidade é constante. Caching, como uma solução para melhorar a latência, desempenha um papel importante. Entretanto, diferentes padrões de caching podem ter impactos significativos na manutenção e complexidade da aplicação. A escolha errada eventualmente pode trazer custos desnecessários.

Entre as abordagens de caching disponíveis, destacam-se:

Cache-Aside (Lazy Loading):

O padrão em que o código da aplicação é responsável por buscar os dados no cache e, em caso de falha (cache miss), recuperar os dados do repositório persistente e armazená-los no cache.

Abaixo trago um exemplo

Write-Through:

Quando uma operação de escrita é realizada, os dados são simultaneamente atualizados tanto no repositório persistente quanto no cache.

Write-Behind (Write-Back):

Os dados são atualizados no cache de forma imediata e, posteriormente, em segundo plano, são persistidos no repositório.

Read-Through:

Similar ao Cache-Aside, mas o repositório persistente é consultado automaticamente pelo sistema de cache em caso de cache miss, sem intervenção direta da aplicação.

Refresh-Ahead:

O cache tenta antecipar cache misses ao atualizar os dados de forma proativa antes que expirem ou sejam solicitados novamente.

Cache Stampede Prevention:

Mecanismos para evitar o problema do “cache stampede”, onde múltiplas requisições simultâneas a um dado expirado sobrecarregam o repositório persistente.

Vantagens do Cache-Aside

Dentre essas opções, destaco Cache-Aside por sua simplicidade e controle, tornando-se a minha escolha primária e padrão para o uso de cache, considerando-a acima de todos os demais padrões.

Vamos explorar essa estratégia em detalhe, comparando-a com as outras abordagens.

Simplicidade e clareza

Uma das maiores vantagens do Cache-Aside é a simplicidade em sua implementação e operação. Com este padrão, o código da aplicação é o responsável direto por gerenciar a interação com o cache. Se os dados não estiverem presentes no cache, a aplicação recupera esses dados do repositório lento (por exemplo, um banco de dados) e, em seguida, armazena-os no cache para acessos futuros.

Essa abordagem oferece clareza, pois o mesmo código que realiza a leitura no cache também é responsável por recuperar e armazenar os dados no repositório persistente. Isso elimina a “mágica” que ocorre em outras estratégias, como o Refresh-Ahead, onde é difícil rastrear o componente responsável por atualizar os dados no cache. No Cache-Aside, o fluxo de dados é explícito e facilmente compreendido por qualquer desenvolvedor que leia o código.









public decimal GetPrice(int productId)
{
  string priceKey = $"cache:product:{productId}:price";
  decimal? returnValue = this.Redis.Get<decimal>(priceKey);
  if(returnValue == null)
  {
    string priceLockKey = $"{priceKey}:lock";
    bool locked = false;     
    using (var lock = this.Redis.Lock(key: priceLockKey, timeout: TimeSpan.FromSeconds(2), retry: 3))
    {
      //AQUI EU COMENTARIA ALGO COMO:

      //NÃO ALTERE, NÃO OTIMIZE, ESSA CHAMADA É INTENCIONALEMNTE REDUNDANTE
      //SE VOCÊ CONSIDERA REMOVÊ-LA, LEIA SOBRE "Double-checked locking"
      // https://en.wikipedia.org/wiki/Double-checked_locking
      returnValue = this.Redis.Get<decimal>(priceKey);
      if(returnValue == null)
      {
        returnValue = this.Repository.GetPriceByProduct(productId);
        this.Redis.Set<decimal>(returnValue);
      }
    }
  }
  return returnValue;
}

Controle total sobre o cache

O Cache-Aside oferece controle granular sobre quais dados são armazenados no cache. A aplicação pode decidir dinamicamente quais informações são mais relevantes para o cache com base nos padrões de acesso. Isso é particularmente útil em cenários onde nem todos os dados têm a mesma frequência de acesso ou criticidade.

Diferentemente de Write-Through e Write-Behind, onde todo dado escrito é armazenado no cache independentemente de sua necessidade futura, o Cache-Aside permite que o cache seja utilizado de forma mais eficiente, evitando o armazenamento de informações que raramente serão reutilizadas. Essa abordagem não apenas economiza memória, mas também minimiza a complexidade de manter o cache atualizado. Isso se traduz em custo menor!

Prevenção de Cache Stampede

O problema do cache stampede ocorre quando múltiplas requisições simultâneas atingem o cache para um dado expirado, levando a uma sobrecarga no repositório persistente. Com o Cache-Aside, é possível implementar estratégias de controle, como bloqueios ou filas, para evitar que múltiplos processos tentem atualizar o mesmo dado ao mesmo tempo.

Além disso, em caso de falha do cache, o Cache-Aside evita o problema de realizar uma grande varredura no repositório lento para repopular o cache, pois os dados só são recarregados conforme necessário. Isso reduz o risco de sobrecarregar o sistema com operações desnecessárias, que é um risco presente em estratégias como Refresh-Ahead.

Redução de sobrecarga com Refresh-Ahead

Embora o Refresh-Ahead tenha a vantagem de tentar evitar cache misses antecipando a necessidade de atualização dos dados, ele também introduz complexidade e riscos. Com o tempo, os componentes que são responsáveis por escrever os dados no cache podem ser negligenciados, pois a escrita no cache ocorre de maneira automática e invisível para a maioria dos desenvolvedores.

Essa falta de visibilidade pode levar à deterioração do desempenho, especialmente se o componente responsável por atualizar o cache se tornar um gargalo ou se for deslocado para fora do ciclo de vida ativo do projeto. Com o Cache-Aside, essa situação é evitada, pois o fluxo de dados é sempre controlado pela aplicação, garantindo que o cache seja mantido de maneira eficaz e eficiente.

Comparação com outras estratégias de cache

Write-Through e Write-Behind: Cacheamento proativo, mas ineficiente

O Write-Through e Write-Behind são estratégias onde cada operação de escrita na base de dados resulta em uma atualização correspondente no cache. Isso parece ideal em termos de manter o cache atualizado, mas pode resultar em um uso ineficiente do cache. Dados que são escritos, mas raramente lidos, ocupam espaço no cache e adicionam uma sobrecarga desnecessária ao sistema.

Além disso, o Write-Behind pode introduzir inconsistências temporárias entre o cache e o repositório persistente, especialmente em cenários de falha, complicando a manutenção e depuração do sistema.

Read-Through: Redução de código, aumento de complexidade

O padrão Read-Through é semelhante ao Cache-Aside, mas com a diferença de que a lógica de recuperação do repositório lento é movida para dentro do sistema de cache. Embora isso reduza a quantidade de código na aplicação, ele também aumenta a complexidade, pois a lógica de negócios é parcialmente delegada ao sistema de cache. Isso pode dificultar a manutenção, especialmente quando a lógica de recuperação é complexa ou específica para o domínio da aplicação.

Além disso, o Read-Through não oferece o mesmo nível de controle granular que o Cache-Aside, pois a aplicação perde a capacidade de decidir dinamicamente quais dados devem ser armazenados ou recuperados do cache.

Refresh-Ahead: Magia que pode sair de controle

O Refresh-Ahead, como mencionado, é uma tentativa de evitar cache misses proativamente, mas seu principal problema reside na falta de visibilidade e controle. Ao automatizar a atualização do cache, os desenvolvedores podem perder de vista o componente responsável por essa tarefa, resultando em possíveis problemas de desempenho e manutenção.

Com o Cache-Aside, o fluxo de dados é claro e explícito, eliminando a “magia” e garantindo que cada operação de leitura e escrita seja compreendida e controlada pela equipe de desenvolvimento.

Cache Stampede Prevention: Uma solução complementar

Embora a prevenção de Cache Stampede não seja uma estratégia de caching em si, é uma preocupação importante ao utilizar qualquer padrão de cache. No caso do Cache-Aside, a implementação de mecanismos para evitar a sobrecarga do repositório persistente é direta e eficaz. A combinação de Cache-Aside com técnicas de prevenção de cache stampede oferece uma solução robusta para manter a performance e a integridade do sistema.

Conclusão

Por isso considero Cache-Aside a estratégia mais segura, eficaz e controlável para a maioria dos casos de uso em sistemas distribuídos. Sua simplicidade e clareza na implementação proporcionam um entendimento claro do fluxo de dados, ao mesmo tempo em que oferecem controle granular sobre o uso do cache. Isso evita a sobrecarga desnecessária do cache com dados que não serão utilizados e protege o sistema contra problemas de desempenho, como cache stampede ou degradação dos componentes de escrita no cache.

Enquanto outras estratégias de caching como Write-Through, Write-Behind e Refresh-Ahead têm seus méritos, elas também introduzem complexidades e riscos que podem comprometer a eficiência e a manutenção a longo prazo. O Cache-Aside, por outro lado, oferece uma solução balanceada que otimiza o uso do cache, mantém o desempenho do sistema e facilita a manutenção, tornando-o a escolha ideal para a maioria dos projetos de software.

Ao adotar o Cache-Aside como estratégia primária de caching, garantimos não apenas a performance e a escalabilidade do sistema, mas também uma base sólida para a manutenção e evolução contínua da aplicação.

O Cloud Native .NET é meu principal projeto.

Onde empenho energia para ajudar, acompanhar, direcionar Desenvolvedores, Líderes Técnicos e jovens Arquitetos na jornada Cloud Native.

Conduzo entregando a maior e mais completa stack de tecnologias do mercado.

Ao trabalhar com desenvolvedores experientes, eu consigo usar seu aprendizado com .NET, banco de dados, e arquitetura para encurtar a jornada.

Ao restringir à desenvolvedores .NET eu consigo usar do contexto de tecnologias e problemas do seu dia-a-dia, coisas que você conhece hoje, como WCF, WebForms, IIS e MVC, por exemplo, para mostrar a comparação entre o que você conhece e o que está sendo apresentado.

É assim que construímos fundamentos sólidos, digerindo a complexidade com didática, tornando o complexo, simples.

É assim que conseguimos tornar uma jornada densa, em um pacote de ~4 meses.

Eu não acredito que um desenvolvedor possa entender uma tecnologia sem compreender seus fundamentos. Ele no máximo consegue ser produtivo, mas isso não faz desse desenvolvedor um bom tomador de decisões técnicas.

É preciso entender os fundamentos para conseguir tomar boas decisões.

0 comentários

Enviar um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Luiz Carlos Faria

Mensagem do Autor

Espero que goste desse post. Não deixe de comentar e falar o que achou.

Se acha que esse post pode ajudar alguém que você conheça, compartilhe!

 

Lives

Fique de olho nas lives

Fique de olho nas lives no meu canal do Youtube, no Canal .NET e nos Grupos do Facebook e Instagram.

Aceleradores

Existem diversas formas de viabilizar o suporte ao teu projeto. Seja com os treinamentos, consultoria, mentorias em grupo.