Diretrizes de cache

O caching é uma técnica comum que tem o objetivo de melhorar o desempenho e a escalabilidade de um sistema. O cache copia temporariamente os dados acessados com frequência para o armazenamento localizado mais perto do aplicativo do que a origem original. Essa abordagem pode melhorar significativamente os tempos de resposta para aplicativos cliente, fornecendo dados mais rapidamente.

O cache é mais eficaz quando uma instância de cliente lê repetidamente os mesmos dados, especialmente se todas as seguintes condições se aplicam ao armazenamento de dados original:

  • O armazenamento de dados permanece relativamente estático.
  • É lento em comparação à velocidade do cache.
  • Está sujeito a um alto nível de contenção.
  • Está longe o suficiente dos clientes para que a latência de rede seja significativa.

Cache em aplicativos distribuídos

Os aplicativos distribuídos normalmente implementam uma ou ambas as estratégias a seguir ao armazenar dados em cache:

  • Use um cache privado, em que você mantém dados localmente no computador que executa um aplicativo ou serviço.

  • Use um cache compartilhado, que serve como uma fonte comum que vários processos e computadores podem acessar.

Em ambos os casos, você pode executar o cache no lado do cliente e no lado do servidor. O processo que fornece a interface do usuário para um sistema, como um navegador da Web ou um aplicativo da área de trabalho, manipula o cache do lado do cliente. O cache do lado do servidor é feito remotamente pelo processo que fornece os serviços de negócios.

Cache privado

O tipo mais básico de cache é um repositório na memória. Ele é mantido no espaço de endereço de um único processo e o código executado nesse processo acessa o cache diretamente. Esse tipo de cache é rápido de acessar. Ele também pode fornecer um meio eficaz para armazenar quantidades modestas de dados estáticos. A quantidade de memória disponível no computador normalmente restringe o tamanho de um cache.

Se você precisar armazenar em cache mais informações do que é fisicamente possível na memória, poderá gravar dados armazenados em cache no sistema de arquivos local. O acesso a dados do sistema de arquivos leva mais tempo do que se os dados forem armazenados na memória, mas ainda devem ser mais rápidos e confiáveis do que recuperar dados em uma rede.

Se você tiver várias instâncias de um aplicativo que usa esse modelo em execução simultaneamente, cada instância do aplicativo terá seu próprio cache independente que contém sua própria cópia dos dados.

Pense em um cache como um instantâneo dos dados originais em algum momento do passado. Se esses dados não forem estáticos, é provável que diferentes instâncias de aplicativo mantenham versões diferentes dos dados em seus caches. Portanto, a mesma consulta executada por essas instâncias pode retornar resultados diferentes, conforme mostrado no diagrama a seguir.

Diagrama mostrando os resultados do uso de um cache na memória em instâncias diferentes de um aplicativo.

Diagrama mostrando a inconsistência de cache em várias instâncias de aplicativo conectadas a um banco de dados SQL compartilhado. À direita está um banco de dados SQL representado como um cilindro. No canto superior esquerdo, há um grande círculo rotulado como instância de aplicativo A e, no canto inferior esquerdo, há um círculo de tamanho semelhante rotulado como instância de aplicativo B. Dentro da instância de aplicativo A, há um ícone de engrenagem que representa o processo do aplicativo. Abaixo do ícone de engrenagem há uma tabela em grade que representa um cache em memória. Fora do limite, há um rótulo com o texto "cache é um instantâneo dos dados no momento X". Dentro da instância do aplicativo B, há outro ícone de engrenagem. Abaixo do ícone de engrenagem há uma tabela em grade que representa um cache em memória. Fora do limite, há um rótulo com o texto "cache é um instantâneo dos dados no momento Y". No banco de dados SQL, uma linha se estende até a instância A do aplicativo, indicando que a instância A do aplicativo recupera dados no momento X e os armazena em cache na memória. Uma segunda linha se estende do banco de dados SQL para a instância B do aplicativo, a instância de aplicativo rotulada B recupera dados no momento Y e os armazena em cache na memória. Entre essas duas linhas de anotação, há uma observação com o texto de que as informações no banco de dados mudam entre o momento X e o momento Y.

Cache compartilhado

Usando um cache compartilhado, você pode garantir que todas as instâncias de aplicativo vejam a mesma exibição de dados armazenados em cache. O cache está localizado em um local separado, que normalmente é hospedado como parte de um serviço separado, conforme mostrado no diagrama a seguir.

Diagrama mostrando os resultados de aplicativos usando um cache compartilhado.

Diagrama mostrando como um serviço de cache compartilhado resolve a inconsistência de cache em várias instâncias de aplicativo conectadas a um banco de dados SQL. Na extrema direita há um banco de dados SQL representado como um cilindro. No centro, há uma caixa rotulada como "serviço de cache compartilhado", contendo uma tabela em grade que representa o cache compartilhado. Na extrema esquerda há um ícone de engrenagem rotulado da instância do aplicativo A. Abaixo dele, há um segundo ícone de engrenagem rotulado da instância do aplicativo B. Uma seta preta aponta do banco de dados SQL para a grade de serviço de cache compartilhado, indicando que o banco de dados preenche o cache compartilhado. No serviço de cache compartilhado, duas setas azuis se estendem para fora, uma em direção à instância A do aplicativo e outra em direção à instância B do aplicativo. Na extrema esquerda, um colchete curvo se estende verticalmente entre as duas instâncias de aplicativo, com um rótulo adjacente lendo "Ambas as instâncias de aplicativo veem os mesmos dados armazenados em cache". Esse rótulo e colchete realçam o principal benefício da arquitetura de cache compartilhado: ao contrário de um cache na memória por instância, as instâncias de aplicativo A e B recuperam dados do mesmo cache centralizado.

Um benefício importante da abordagem de cache compartilhado é a escalabilidade. Muitos serviços de cache compartilhado são implementados usando um cluster de servidores e usam software para distribuir os dados pelo cluster de forma transparente. Uma instância de aplicativo envia uma solicitação para o serviço de cache. A infraestrutura subjacente determina a localização dos dados armazenados em cache no cluster. Você pode dimensionar facilmente o cache adicionando mais servidores.

Há duas desvantagens principais da abordagem de cache compartilhado:

  • O cache é mais lento para acessar porque ele não é mais mantido localmente com cada instância do aplicativo.
  • Implementar um serviço de cache separado pode adicionar complexidade à solução.

Considerações sobre como usar o cache

As seções a seguir descrevem considerações para projetar e usar um cache.

Decidir quando armazenar dados em cache

O cache pode melhorar drasticamente o desempenho, a escalabilidade e a disponibilidade. Quanto mais dados você tiver e maior o número de usuários que precisam acessar esses dados, maiores serão os benefícios do cache. O cache reduz a latência e a contenção associadas ao tratamento de grandes volumes de solicitações simultâneas no armazenamento de dados original.

Por exemplo, um banco de dados pode dar suporte a um número limitado de conexões simultâneas. A recuperação de dados de um cache compartilhado, no entanto, em vez do banco de dados subjacente, possibilita que um aplicativo cliente acesse esses dados mesmo que o número de conexões disponíveis esteja esgotado no momento. Além disso, se o banco de dados ficar indisponível, os aplicativos cliente poderão continuar usando os dados no cache.

Considere armazenar em cache dados lidos com frequência, mas modificados com pouca frequência. Por exemplo, armazenar em cache dados que têm uma proporção maior de operações de leitura do que operações de gravação. No entanto, não use o cache como o repositório autoritativo de informações críticas. Em vez disso, salve todas as alterações importantes em um armazenamento de dados persistente. Se o cache não estiver disponível, seu aplicativo ainda poderá continuar operando usando o armazenamento de dados e você não perderá informações importantes.

Determinar como armazenar dados em cache efetivamente

Para usar um cache com eficiência, determine os dados mais apropriados para armazenar em cache e armazene-os em cache no momento certo. Você pode adicionar dados ao cache quando um aplicativo os recupera pela primeira vez. O aplicativo precisa buscar os dados apenas uma vez do armazenamento de dados e, em seguida, o acesso subsequente pode ser atendido usando o cache.

Como alternativa, você pode preencher parcial ou totalmente um cache com dados com antecedência, normalmente quando o aplicativo é iniciado. Essa abordagem é conhecida como seeding. No entanto, não é aconselhável implementar a propagação em um cache grande, pois essa abordagem pode impor uma carga elevada e repentina sobre o repositório de dados original quando o aplicativo começa a ser executado.

Analise os padrões de uso para decidir se deseja pré-preencher um cache total ou parcialmente e escolher quais dados armazenar em cache. Por exemplo, você pode preencher o cache com os dados estáticos do perfil do usuário para clientes que usam o aplicativo todos os dias, mas não para clientes que usam o aplicativo apenas uma vez por semana.

O cache normalmente funciona bem com dados imutáveis ou que são alterados com pouca frequência. Exemplos incluem informações de referência, como informações de produtos e preços em um aplicativo de comércio eletrônico, ou recursos estáticos compartilhados que são caros de construir. Carregue alguns ou todos esses dados no cache na inicialização do aplicativo para minimizar a demanda por recursos e melhorar o desempenho. Talvez você também queira ter um processo em segundo plano que atualize periodicamente os dados de referência no cache para garantir que eles estejam atualizados. Ou, o processo em segundo plano pode atualizar o cache quando os dados de referência são alterados.

O cache é menos útil para dados dinâmicos, embora haja algumas exceções. Para obter mais informações, consulte a seção de dados altamente dinâmicos do Cache mais adiante neste artigo. Quando os dados originais são alterados regularmente, as informações armazenadas em cache ficam obsoletas rapidamente ou a sobrecarga de sincronizar o cache com o armazenamento de dados original reduz a eficácia do cache.

Um cache não precisa incluir os dados completos de uma entidade. Por exemplo, se um item de dados representar um objeto multivalorizado, como um cliente de banco que tenha um nome, endereço e saldo de conta, alguns desses elementos poderão permanecer estáticos, como o nome e o endereço. Outros elementos, como o saldo da conta, podem ser mais dinâmicos. Nessas situações, pode ser útil armazenar em cache as partes estáticas dos dados e recuperar (ou calcular) apenas as informações restantes quando necessário.

Realize testes de desempenho e análise de uso para determinar se o pré-preenchimento do cache, o carregamento sob demanda ou uma combinação de ambos é apropriado. Baseie a decisão no padrão de volatilidade e uso dos dados. A utilização do cache e a análise de desempenho são importantes em aplicativos que encontram cargas pesadas e devem ser altamente escalonáveis. Por exemplo, em cenários altamente escalonáveis, você pode semear o cache para reduzir a carga no armazenamento de dados em horários de pico.

O cache também pode ser usado para evitar a repetição de cálculos enquanto o aplicativo está em execução. Se uma operação transformar dados ou executar um cálculo complicado, ela poderá salvar os resultados da operação no cache. Se o mesmo cálculo for necessário posteriormente, o aplicativo poderá recuperar os resultados do cache.

Um aplicativo pode modificar dados que estão em um cache. No entanto, considere o cache como um armazenamento de dados transitório que pode desaparecer a qualquer momento. Não armazene dados valiosos somente no cache; Certifique-se de manter as informações no armazenamento de dados original também. Essa abordagem minimiza a chance de perder dados se o cache ficar indisponível.

Manter o cache consistente durante as gravações

Quando o aplicativo altera os dados que ele também armazena em cache, decida como o cache permanece consistente com o sistema de registro. Duas abordagens são comuns:

  • Invalidação na gravação: grave a alteração no armazenamento de dados e remova a entrada de cache afetada. A próxima leitura preenche novamente o cache a partir do repositório de dados. Essa abordagem mantém o caminho de gravação simples, mas um leitor pode experimentar uma falha de cache ou ver brevemente dados obsoletos imediatamente após uma gravação. O padrão Cache-Aside descreve essa abordagem.

  • Write-through: Atualiza-se o repositório de dados e o cache como parte da mesma operação de gravação, e a resposta à gravação é retornada somente depois que ambas as atualizações forem bem-sucedidas. Essa abordagem fornece aos leitores o valor atualizado imediatamente após uma gravação bem-sucedida, ao custo de maior latência de gravação e mais lógica de coordenação.

    Para obter um exemplo completo que usa o Azure Functions para coordenar atualizações de gravação no Banco de Dados SQL do Azure e no Redis Gerenciado do Azure, consulte Cache de gravação direta com o Redis Gerenciado do Azure e o Banco de Dados SQL do Azure.

Use a gravação somente para caminhos de acesso com predominância de leitura que precisam de dados atualizados imediatamente após a gravação. Para dados que raramente são lidos depois de gravados ou que mudam constantemente, invalide na gravação ou ignore o cache.

Armazenar dados altamente dinâmicos em cache

Armazenar informações em rápida alteração em um repositório de dados persistente pode impor uma sobrecarga ao sistema. Por exemplo, considere um dispositivo que relata continuamente o status ou alguma outra medida. Se um aplicativo optar por não armazenar esses dados em cache com base em que as informações armazenadas em cache geralmente estão desatualizadas, a mesma consideração poderá ser verdadeira ao armazenar e recuperar essas informações do armazenamento de dados. No tempo necessário para salvar e buscar esses dados, ele pode mudar.

Em uma situação como essa, considere os benefícios de armazenar as informações dinâmicas diretamente no cache, em vez de no armazenamento de dados persistente. Se os dados não forem críticos e não exigirem auditoria, não importará se a alteração ocasional for perdida.

Gerenciar a expiração de dados em um cache

Na maioria dos casos, um cache contém dados que são uma cópia dos dados do armazenamento de dados original. Os dados no armazenamento de dados original podem ser alterados depois que você os armazena em cache, o que pode tornar os dados armazenados em cache obsoletos. Muitos sistemas de cache permitem configurar o cache para expirar os dados, o que reduz o período durante o qual os dados podem estar desatualizados.

Depois que os dados armazenados em cache expirarem, o cache os removerá. Em seguida, o aplicativo recupera novos dados do armazenamento de dados original e pode substituir os dados expirados no cache. Você pode definir uma política de expiração padrão ao configurar o cache. Em muitos serviços de cache, você também pode especificar o período de expiração para objetos individuais ao armazená-los programaticamente no cache. Alguns caches permitem que você especifique o período de expiração como um valor absoluto ou como um valor deslizante se o item não for acessado dentro de um tempo especificado. Essa configuração substitui qualquer política de expiração em todo o cache, mas apenas para os objetos especificados.

Note

Considere o período de expiração para o cache e os objetos que ele contém cuidadosamente. Se você o tornar muito curto, os objetos expirarão muito rapidamente e você reduzirá os benefícios de usar o cache. Se você tornar o período muito longo, você corre o risco de os dados ficarem obsoletos.

Se você permitir que os dados permaneçam no cache por um longo tempo, o cache poderá ser preenchido. Nesse caso, qualquer solicitação para adicionar novos itens ao cache pode fazer com que o cache remova à força alguns itens em um processo conhecido como remoção. Os serviços de cache normalmente dão preferência à remoção de dados LRU (usados menos recentemente), mas geralmente você pode substituir essa política e impedir que determinados itens sejam removidos. No entanto, se você adotar essa abordagem, correrá o risco de exceder a memória disponível no cache. Um aplicativo que tenta adicionar um item a um cache completo falha com uma exceção.

Algumas implementações de cache podem fornecer outras políticas de remoção. Os tipos de políticas de remoção incluem:

  • Uma política usada mais recentemente: remove os itens usados mais recentemente do cache na expectativa de que os dados não serão necessários novamente.
  • Uma política inicial: remove os dados mais antigos do cache primeiro.
  • Uma política de remoção explícita: remove itens do cache com base em um evento de gatilho, como os dados que estão sendo modificados.

Invalidar dados em um cache do lado do cliente

Os dados mantidos em um cache do lado do cliente são considerados fora do controle do serviço que fornece os dados para o cliente. Um serviço não pode forçar diretamente um cliente a adicionar ou remover informações de um cache do lado do cliente.

Essa limitação significa que um cliente que usa um cache mal configurado pode continuar usando informações desatualizadas. Por exemplo, se as políticas de expiração do cache não forem implementadas corretamente, um cliente poderá usar informações desatualizadas armazenadas em cache localmente quando as informações na fonte de dados original forem alteradas.

Se você criar um aplicativo Web que fornece dados por meio de uma conexão HTTP, poderá forçar implicitamente um cliente Web, como um navegador ou proxy Web, a buscar as informações mais recentes. Você pode disparar essa atualização alterando o URI sempre que atualizar o recurso. Os clientes Web normalmente usam o URI de um recurso como a chave no cache do lado do cliente, portanto, se o URI for alterado, o cliente Web ignorará as versões armazenadas em cache anteriormente de um recurso e buscará a nova versão.

Gerenciar simultaneidade em um cache

Muitas vezes, você projeta caches a serem compartilhados por várias instâncias de um aplicativo. Cada instância de aplicativo pode ler e modificar dados no cache, portanto, os mesmos problemas de simultaneidade que surgem com qualquer armazenamento de dados compartilhado também se aplicam a um cache. Em uma situação em que um aplicativo precisa modificar os dados mantidos no cache, talvez seja necessário garantir que as atualizações feitas por uma instância do aplicativo não substituam as alterações feitas por outra instância.

Dependendo da natureza dos dados e da probabilidade de colisões, adote uma das duas abordagens para simultaneidade:

  • Otimista: Antes que o aplicativo atualize os dados, ele verifica se os dados no cache foram alterados desde que foram recuperados. Se os dados ainda forem os mesmos, o aplicativo fará a alteração. Caso contrário, o aplicativo decidirá se o atualizará. A lógica de negócios que impulsiona essa decisão é específica do aplicativo. Essa abordagem é adequada para situações em que as atualizações são pouco frequentes ou em que é improvável que ocorram colisões.

  • Pessimista: Quando o aplicativo recupera os dados, ele bloqueia os dados no cache para impedir que outra instância os altere. Esse processo garante que colisões não possam ocorrer, mas também pode bloquear outras instâncias que precisam processar os mesmos dados. A simultaneidade pessimista pode afetar a escalabilidade de uma solução e é recomendada apenas para operações de curta duração. Essa abordagem pode ser apropriada para situações em que colisões são mais prováveis, especialmente se um aplicativo atualiza vários itens no cache e deve garantir que essas alterações sejam aplicadas de forma consistente.

Implementar alta disponibilidade e escalabilidade e melhorar o desempenho

Evite usar um cache como o repositório primário de dados. O armazenamento de dados original, do qual o cache é preenchido, atende a essa função. O armazenamento de dados original garante a persistência de dados.

Tenha cuidado para não introduzir dependências críticas sobre a disponibilidade de um serviço de cache compartilhado em suas soluções. Um aplicativo deve ser capaz de continuar funcionando se o cache compartilhado não estiver disponível. O aplicativo não deve ficar sem resposta ou falhar enquanto aguarda o serviço de cache ser retomado.

Portanto, o aplicativo deve estar preparado para detectar a disponibilidade do serviço de cache e retornar ao armazenamento de dados original se o cache estiver inacessível. O padrão Circuit-Breaker é útil para lidar com esse cenário. O serviço que fornece o cache pode ser recuperado e, depois de ficar disponível, o cache pode ser repovoado à medida que os dados são lidos do armazenamento de dados original, seguindo uma estratégia como o padrãoCache-Aside.

No entanto, a escalabilidade do sistema poderá ser afetada se o aplicativo voltar ao armazenamento de dados original quando o cache estiver temporariamente indisponível. Enquanto o cache está sendo recuperado, o armazenamento de dados original pode ser inundado com solicitações de dados, resultando em tempos limite e conexões com falha.

Considere implementar um cache local privado em cada instância de um aplicativo, juntamente com o cache compartilhado que todas as instâncias do aplicativo acessam. Quando o aplicativo recupera um item, ele pode verificar primeiro em seu cache local, depois no cache compartilhado e, por fim, no armazenamento de dados original. O cache local pode ser preenchido usando os dados no cache compartilhado ou no banco de dados se o cache compartilhado não estiver disponível.

Essa abordagem requer uma configuração cuidadosa para impedir que o cache local se torne muito obsoleto em relação ao cache compartilhado. No entanto, o cache local atuará como um buffer se o cache compartilhado estiver inacessível, conforme mostrado no diagrama a seguir.

Diagrama mostrando a estrutura de um cache compartilhado que pode usar um cache privado local como um buffer.

Diagrama mostrando uma arquitetura de cache híbrido. Na extrema direita há um banco de dados SQL representado como um cilindro. Na área superior direita, há um grande círculo rotulado como serviço de cache compartilhado, contendo uma tabela em grade que representa o cache compartilhado. Uma seta aponta para a esquerda do banco de dados SQL para a grade de serviço de cache compartilhado, indicando que o banco de dados preenche o cache compartilhado. No canto superior esquerdo há um grande círculo rotulado como "instância de aplicativo A". Dentro do círculo, um ícone de engrenagem está na parte superior e, abaixo dele, há uma tabela em grade que representa um cache privado local. Uma seta preta de duas extremidades conecta o ícone de engrenagem à grade do cache local, indicando uma relação de leitura e gravação entre o processo do aplicativo e seu cache local. No canto inferior esquerdo há um círculo grande identificado como instância de aplicativo B, com a mesma estrutura interna. A partir do serviço de cache compartilhado, duas setas azuis se estendem para fora: uma inclinada para cima em direção ao ícone de engrenagem na instância de aplicativo A e outra inclinada para baixo em direção ao ícone de engrenagem na instância de aplicativo B. Uma linha preta retangular forma um caminho de fallback que vai do banco de dados SQL ao longo das bordas superior e direita, depois desce e segue pela parte inferior, conectando-se novamente às grades de cache local em ambas as instâncias de aplicativo. Um rótulo no centro inferior do diagrama lê, se o serviço de cache compartilhado não estiver disponível, a lógica do aplicativo preencherá o cache local do banco de dados. Na extrema esquerda, um colchete curvo se estende entre as instâncias de aplicativo A e B, com uma leitura de rótulo adjacente, se o serviço de cache compartilhado não estiver disponível, as instâncias de aplicativo poderão continuar a funcionar usando caches locais e privados.

Para dar suporte a caches grandes que contêm dados relativamente de longa duração, alguns serviços de cache fornecem uma opção de alta disponibilidade que implementa o failover automático se o cache ficar indisponível. Essa abordagem normalmente envolve a replicação dos dados armazenados em cache de um servidor de cache primário para um servidor de cache secundário, e a alternância para o servidor secundário se o servidor primário falhar ou a conectividade for perdida.

Para reduzir a latência proveniente da gravação em vários destinos, a replicação para o servidor secundário pode ocorrer de forma assíncrona quando os dados são gravados no cache no servidor primário. Essa abordagem leva à possibilidade de que algumas informações armazenadas em cache possam ser perdidas se houver uma falha, mas a proporção desses dados deve ser pequena, em comparação com o tamanho geral do cache.

Se um cache compartilhado for grande, pode ser benéfico particionar os dados armazenados em cache entre nós para reduzir as chances de contenção e melhorar a escalabilidade. Muitos caches compartilhados dão suporte à capacidade de adicionar e remover nós dinamicamente e rebalancear os dados entre partições. Essa abordagem pode envolver clustering, no qual a coleção de nós é apresentada a aplicativos cliente como um único cache. Internamente, no entanto, os dados são dispersos entre nós seguindo uma estratégia de distribuição predefinida que equilibra a carga uniformemente. Para obter mais informações, consulte o padrão de fragmentação.

O clustering também pode aumentar a disponibilidade do cache. Se um nó falhar, o restante do cache ainda poderá ser acessado. O agrupamento costuma ser usado em conjunto com replicação e failover. Cada nó pode ser replicado, e a réplica pode ser colocada online rapidamente se o nó falhar.

Muitas operações de leitura e gravação provavelmente envolverão valores de dados únicos ou objetos. No entanto, às vezes pode ser necessário armazenar ou recuperar grandes volumes de dados rapidamente. Por exemplo, a propagação de um cache pode envolver a gravação de centenas ou milhares de itens no cache. Um aplicativo também pode precisar recuperar um grande número de itens relacionados do cache como parte da mesma solicitação.

Muitos caches em grande escala fornecem operações em lote para essas finalidades. Esse recurso permite que um aplicativo cliente empacote um grande volume de itens em uma única solicitação, o que reduz a sobrecarga de executar um grande número de solicitações pequenas.

Caching e consistência eventual.

Para que o padrão Cache-Aside funcione, a instância do aplicativo que preenche o cache deve ter acesso à versão mais recente e consistente dos dados. Em um sistema que implementa a consistência eventual (como um armazenamento de dados replicado), essa condição pode não ser verdadeira.

Uma instância de um aplicativo pode modificar um item de dados e invalidar a versão armazenada em cache desse item. Outra instância do aplicativo pode tentar ler este item de um cache, o que causa uma falha no cache. Em seguida, ele lê os dados do armazenamento de dados e os adiciona ao cache. No entanto, se o armazenamento de dados não estiver totalmente sincronizado com as outras réplicas, a instância do aplicativo poderá ler e preencher o cache com o valor antigo.

Um cache distribuído apresenta outra camada para esse problema. O teorema CAP afirma que um sistema distribuído pode fornecer no máximo duas das três garantias: consistência, disponibilidade e tolerância à partição. Como as partições de rede são inevitáveis em ambientes de nuvem, você deve escolher entre consistência e disponibilidade. A maioria dos caches distribuídos, incluindo o Redis, prioriza a disponibilidade e a resiliência a partições em relação à consistência forte. Essa prioridade significa que as leituras de uma réplica de cache podem retornar dados obsoletos durante uma partição de rede ou imediatamente após uma gravação em um nó diferente. Ao projetar sua estratégia de cache, decida quanta obsolescência dos dados seu aplicativo pode tolerar e defina os valores de TTL (tempo de vida) de acordo com isso. Para dados que devem ser atuais, use TTLs mais curtos ou ignore totalmente o cache e leia do armazenamento de dados de origem.

Para obter mais informações sobre como lidar com a consistência de dados em sistemas distribuídos, consulte Considerações sobre dados para microsserviços.

Proteger dados armazenados em cache

Independentemente do serviço de cache usado, considere como proteger os dados no cache contra acesso não autorizado. Duas das principais preocupações são:

  • A privacidade dos dados no cache.
  • A privacidade dos dados conforme eles fluem entre o cache e o aplicativo que está usando o cache.

Para proteger os dados no cache, o serviço de cache pode implementar um mecanismo de autenticação que exige que os aplicativos especifiquem os seguintes detalhes:

  • Quais identidades podem acessar dados no cache.
  • Quais operações de leitura e gravação essas identidades têm permissão para executar.

Para reduzir a sobrecarga associada à leitura e gravação de dados, depois que uma identidade recebe acesso de gravação ou leitura ao cache, essa identidade pode usar qualquer dado no cache.

Se você precisar restringir o acesso a subconjuntos dos dados armazenados em cache, use uma das seguintes abordagens:

  • Divida o cache em partições usando diferentes servidores de cache. Conceda apenas acesso a identidades para as partições que elas devem ter permissão para usar.

  • Criptografe os dados em cada subconjunto usando chaves diferentes. Forneça as chaves de criptografia apenas para identidades que devem ter acesso a cada subconjunto. Um aplicativo cliente ainda pode recuperar todos os dados no cache, mas só pode descriptografar os dados para os quais ele tem as chaves.

Você também deve proteger os dados à medida que eles fluem para dentro e para fora do cache. Depende dos recursos de segurança fornecidos pela infraestrutura de rede que os aplicativos cliente usam para se conectar ao cache. Se o cache for implementado usando um servidor local na mesma organização que hospeda os aplicativos cliente, o isolamento da rede em si talvez não exija que você execute mais etapas. Se o cache estiver localizado remotamente e exigir uma conexão TCP ou HTTP em uma rede pública, como a Internet, considere implementar o TLS.

Implemente o cache usando o Redis Gerenciado do Azure

As seções restantes deste artigo descrevem como implementar os padrões de cache usando Azure Redis Gerenciado. O Redis Gerenciado do Azure é um serviço redis gerenciado que você pode usar como um cache compartilhado entre instâncias de aplicativo. Oferece suporte ao cache de chave-valor, estruturas de dados, como conjuntos, conjuntos classificados e listas, e persistência opcional para durabilidade entre reinicializações.

Para obter informações sobre camadas disponíveis, planejamento de capacidade, rede e detalhes do recurso, consulte a documentação do Redis Gerenciado do Azure.

Conectar e configurar aplicativos cliente

O Redis dá suporte a aplicativos cliente em muitas linguagens de programação. Para aplicativos .NET, várias bibliotecas de clientes estão disponíveis, cada uma adequada para cargas de trabalho diferentes do Redis. Sua escolha de biblioteca depende se você está usando o Redis estritamente como um cache ou como uma plataforma de dados de vários modelos.

Para se conectar a um servidor Redis, use o método estático Connect da ConnectionMultiplexer classe. A conexão criada por esse método é criada para uso durante todo o tempo de vida do aplicativo cliente. Vários threads simultâneos podem usar a mesma conexão. Não reconecte e desconecte toda vez que realizar uma operação do Redis, pois isso pode prejudicar o desempenho.

Para obter exemplos de conexão específicos da linguagem, consulte Use Azure Managed Redis no .NET Core.

Escolher uma biblioteca de clientes do .NET

Ao usar o Azure Managed Redis para armazenamento em cache, use as seguintes bibliotecas .NET:

  • StackExchange.Redis: Um cliente Redis de baixo nível que fornece alto desempenho. Use-o quando precisar de acesso direto aos comandos Redis, operações atômicas, transações, pipelining ou scripts Lua.

  • Microsoft. Extensions.Caching.StackExchangeRedis: fornece uma integração IDistributedCache para ASP.NET Core. Use-o para cache de chave-valor simples em que os valores são armazenados como matrizes de bytes opacas. Essa abstração não expõe estruturas de dados avançadas do Redis.

Essas bibliotecas fornecem os primitivos necessários para criar padrões comuns de cache, mas o aplicativo deve implementar a lógica de cache em si.

Implementar padrões de cache

A maneira mais simples de usar o Redis para cache é armazenar valores em chaves usando o modelo chave-valor. Os valores podem ser cadeias de caracteres ou dados binários de comprimento arbitrário, tornando o Redis adequado para armazenar objetos serializados em cache, dados de configuração, estado de sessão ou resultados pré-compilados.

Crie seu keyspace com cuidado e use chaves significativas (mas não verbosas). Por exemplo, use chaves estruturadas como customer:100 (em vez de apenas 100) para representar a chave para o cliente que tem a ID 100. Esse esquema permite distinguir entre valores que armazenam diferentes tipos de dados. Por exemplo, você também pode usar a chave orders:100 para representar a chave para a ordem que tem a ID 100.

Embora as cadeias de caracteres sejam a abordagem de cache mais comum, o Redis dá suporte a um conjunto avançado de tipos de dados nativos, como hashes, listas, conjuntos, conjuntos classificados e fluxos, que permitem padrões de cache mais flexíveis. Para obter mais informações sobre os tipos de dados redis, consulte a documentação do Redis sobre tipos de dados.

Implementar o padrão Cache-Aside

Conforme descrito em Determinar como armazenar dados em cache efetivamente, uma abordagem comum é carregar dados no cache sob demanda. O exemplo a seguir verifica primeiro o cache, recupera da fonte de dados caso não encontre e armazena o resultado para solicitações subsequentes.

var config = new ConfigurationOptions();
// ... configure endpoint, credentials, TLS, etc.
ConnectionMultiplexer redisHostConnection = ConnectionMultiplexer.Connect(config);
IDatabase cache = redisHostConnection.GetDatabase();

async Task<string> RetrieveItemAsync(string itemKey)
{
    // Attempt to retrieve the item from the Redis cache
    string itemValue = await cache.StringGetAsync(itemKey);

    // If the value returned is null, the item was not found in the cache
    // So retrieve the item from the data source and add it to the cache
    if (itemValue is null)
    {
        itemValue = await GetItemFromDataSourceAsync(itemKey);
        await cache.StringSetAsync(itemKey, itemValue);
    }

    return itemValue;
}

Executar operações atômicas e em lotes

Quando vários clientes ou instâncias de aplicativo compartilham um cache, você precisa impedir que atualizações simultâneas corrompam dados. As estratégias gerais de simultaneidade são descritas no Gerenciamento de simultaneidade em um cache anteriormente neste artigo. O Redis fornece vários mecanismos que implementam essas estratégias.

  • Operações de chave única atômica: Use comandos para atualizar um valor em uma única etapa, eliminando as condições de corrida que ocorrem quando GET e SET são emitidas separadamente.

    • INCR, INCRBY, DECRDECRBY incremente ou decremente atomicamente um valor numérico. No StackExchange.Redis, use IDatabase.StringIncrementAsync e IDatabase.StringDecrementAsync. Esses comandos são úteis para contadores, limitadores de taxa e controle de cotas em que vários clientes atualizam a mesma chave simultaneamente.

    • GETSET define atomicamente uma chave para um novo valor e retorna o valor anterior. No StackExchange.Redis, use IDatabase.StringGetSetAsync:

      string oldValue = await cache.StringGetSetAsync("data:counter", 0);
      
  • Operações com várias chaves:MGET e MSET leem ou gravam vários valores de string em uma única ida e volta, reduzindo a sobrecarga de rede quando você precisa trabalhar com várias chaves ao mesmo tempo. Os métodos IDatabase.StringGetAsync e IDatabase.StringSetAsync são sobrecarregados para dar suporte a essa funcionalidade.

    // Create a list of key-value pairs
    var keysAndValues =
        new KeyValuePair<RedisKey, RedisValue>[]
        {
            new("data:key1", "value1"),
            new("data:key99", "value2"),
            new("data:key322", "value3")
        };
    
    // Store the list of key-value pairs in the cache
    await cache.StringSetAsync(keysAndValues);
    ...
    // Find all values that match a list of keys
    RedisKey[] keys = ["data:key1", "data:key99", "data:key322"];
    // Values should contain { "value1", "value2", "value3" }
    RedisValue[] values = await cache.StringGetAsync(keys);
    
  • Transações (simultaneidade otimista): Você pode usar o WATCH comando para monitorar uma ou mais chaves antes de iniciar uma transação comMULTI/EXEC . Se alguma chave observada for alterada antes do início da transação, o Redis descartará a transação e o cliente poderá tentar novamente. A biblioteca StackExchange fornece suporte para transações por meio da ITransaction interface.

    Você cria um ITransaction objeto usando o IDatabase.CreateTransaction método. Você invoca comandos para a transação usando os métodos fornecidos pelo ITransaction objeto.

    A ITransaction interface fornece acesso a um conjunto de métodos semelhante aos métodos acessados pela IDatabase interface, exceto que todos os métodos são assíncronos. Isso significa que eles só são executados quando o ITransaction.Execute método é invocado. O valor retornado pelo ITransaction.Execute método indica se a transação foi criada com êxito (true) ou se falhou (false).

    O snippet de código a seguir mostra um exemplo em que ocorre incremento e decremento a dois contadores como parte da mesma transação:

    ITransaction transaction = cache.CreateTransaction();
    
    var tx1 = transaction.StringIncrementAsync("data:counter1");
    var tx2 = transaction.StringDecrementAsync("data:counter2");
    
    bool result = await transaction.ExecuteAsync();
    
    Console.WriteLine($"Transaction {(result ? "succeeded" : "failed")}");
    
    if (result)
    {
        long increment = await tx1;
        long decrement = await tx2;
    
        Console.WriteLine($"Result of increment: {increment}");
        Console.WriteLine($"Result of decrement: {decrement}");
    }
    

    As transações redis são diferentes de transações em bancos de dados relacionais. O Execute método enfileira todos os comandos que compõem a transação a ser executada e, se qualquer comando não for válido, a transação será interrompida. Se todos os comandos forem enfileirados com êxito, cada comando será executado de forma assíncrona. Se algum comando falhar, os outros ainda continuarão processando. Se você precisar verificar se um comando foi concluído com êxito, busque os resultados usando a Result propriedade da tarefa correspondente, conforme mostrado no exemplo anterior.

  • Script de Lua. Para atualizações em várias etapas que devem ser atômicas em várias chaves, você pode executar um script em Lua no servidor. O Redis executa todo o script como uma única operação sem intercalar outros comandos.

    Note

    Em implantações clusterizadas, todas as chaves envolvidas em uma transação ou script Lua devem residir no mesmo slot de hash. Use marcas de hash, como customer:{123}:name ou customer:{123}:email para colocar chaves relacionadas.

Executar operações de cache do tipo disparar e esquecer

Quando uma atualização de cache não afeta a correção do aplicativo, como incrementar um contador de exibição ou atualizar uma estatística não crítica, você pode ignorar a espera pela resposta do servidor. Em uma operação de cache do tipo disparar e esquecer, seu aplicativo inicia uma tarefa em segundo plano e continua sem esperar que ela seja concluída. O Redis oferece suporte a operações do tipo disparar e esquecer, que reduzem a latência de ida e volta para o cliente, por meio de sinalizadores de comando:

await cache.StringSetAsync("data:key1", 99);
...
cache.StringIncrement("data:key1", flags: CommandFlags.FireAndForget);

Especificar chaves que expiram automaticamente

As estratégias de expiração descritas em Gerenciar expiração de dados em um cache são implementadas no Redis por meio de TTLs por chave. Ao armazenar um item em um cache Redis, você pode especificar um tempo limite após o qual o item é removido automaticamente. Você também pode consultar quanto tempo uma chave tem antes de expirar usando o TTL comando. Esse comando está disponível para aplicativos StackExchange por meio do IDatabase.KeyTimeToLive método.

O snippet de código a seguir mostra como definir um tempo de expiração de 20 segundos em uma chave e consultar o tempo de vida restante da chave:

// Add a key with an expiration time of 20 seconds
await cache.StringSetAsync("data:key1", 99, TimeSpan.FromSeconds(20));
...
// Query how much time a key has left to live
// If the key has already expired, the KeyTimeToLive function returns null
TimeSpan? expiry = cache.KeyTimeToLive("data:key1");

Você também pode definir a expiração para uma data e hora específicas usando o EXPIREAT comando, que está disponível na biblioteca StackExchange como o KeyExpireAsync método. Ele usa um DateTime parâmetro:

await cache.StringSetAsync("data:key1", 99);
await cache.KeyExpireAsync("data:key1",
    new DateTime(2026, 9, 1, 0, 0, 0, DateTimeKind.Utc));

Dica

Você pode remover manualmente um item do cache usando o DEL comando, que está disponível por meio da biblioteca StackExchange como o IDatabase.KeyDeleteAsync método.

Quando o Redis atinge seu limite de memória, ele remove chaves de acordo com uma política de remoção configurada. A política padrão é volatile-lru, que desaloca a chave menos usada recentemente que possui um TTL definido. Outras políticas incluem allkeys-lru, volatile-randome noeviction (o que faz com que as operações de gravação falhem quando a memória estiver cheia). Escolha uma política de remoção com base em se seu aplicativo usa TTLs consistentemente e se você prefere proteger chaves que não têm expiração. Para obter mais informações, consulte o gerenciamento de memória.

Correlacionar itens armazenados em cache

Quando você armazena em cache itens relacionados, geralmente precisa encontrá-los por relação e não apenas pela chave primária. Por exemplo, você pode armazenar em cache postagens de blog e precisar responder a consultas como "quais postagens compartilham a tag Y?" ou "quais tags pertencem ao post X?"

No Redis Gerenciado do Azure, a abordagem recomendada é usar RedisJSON e RediSearch. Armazene cada item armazenado em cache como um documento JSON com seus metadados e crie um índice RediSearch sobre os campos que você precisa consultar. O RediSearch manipula pesquisas inversas, filtragem baseada em marca, consultas de intervalo e pesquisa de texto completo sem exigir que seu aplicativo mantenha estruturas de índice separadas.

Para cenários mais simples, você também pode usar os Conjuntos Redis para criar índices avançados e reversos manualmente. Armazene um conjunto por post (com as tags) e um conjunto por tag (contendo os IDs dos posts):

foreach (BlogPost post in posts)
{
    string postTagsKey = $"blog:posts:{post.Id}:tags";
    await cache.SetAddAsync(
        postTagsKey, post.Tags.Select(s => (RedisValue)s).ToArray());

    foreach (var tag in post.Tags)
    {
        await cache.SetAddAsync($"tag:{tag}:blog:posts", post.Id);
    }
}

Em seguida, você pode consultar marcas para uma postagem usando SetMembersAsync, localizar marcas comuns entre postagens usando SetCombineAsync(SetOperation.Intersect, ...)ou localizar todas as postagens para uma determinada marca. A desvantagem é que seu aplicativo precisa manter os conjuntos direto e reverso, o que aumenta a complexidade à medida que o número de relações cresce.

Localizar itens acessados recentemente

Muitos aplicativos precisam acompanhar os itens acessados ou exibidos mais recentemente. Por exemplo, um site de blogs pode exibir as postagens lidas mais recentemente em um visitante que retorna. As Listas Redis fornecem uma maneira eficiente de implementar padrões de cache baseados em recência. Os itens podem ser adicionados a qualquer extremidade da lista usando LPUSH ou RPUSH, e removidos usando LPOP ou RPOP. Use LTRIM para limitar o comprimento da lista e evitar o crescimento descontrolado da memória.

Implementar um placar de líderes

Os ZSETs (Conjuntos Classificados redis) mantêm classificações ordenadas associando cada elemento a uma pontuação numérica. O Redis mantém a ordenação automaticamente. ZADD é O(log N) e consultas de intervalo, como ZRANGE e ZREVRANGE são O(log N + M),onde M é o número de elementos retornados, portanto, os conjuntos classificados permanecem eficientes mesmo com contagens de itens grandes.

Adicionar itens a um placar de líderes

O exemplo a seguir demonstra como adicionar uma postagem de blog e sua pontuação a um placar de líderes usando o ZADD comando por meio de SortedSetAddAsync:

var db = connection.GetDatabase();
string redisKey = "blog:post_rankings";

BlogPost blogPost = ...; // The blog post being ranked

await db.SortedSetAddAsync(redisKey, blogPost.Title, blogPost.Score);
Recuperar itens classificados

Você pode recuperar itens em ordem de pontuação crescente usando SortedSetRangeByRankWithScoresAsync:

var entries = await db.SortedSetRangeByRankWithScoresAsync(redisKey);

foreach (var entry in entries)
{
    Console.WriteLine($"{entry.Element}: {entry.Score}");
}

Note

SortedSetRangeByRankAsync retorna apenas valores de membro, não pontuações.

Recuperar os principais N itens

Para obter os itens de maior pontuação, como as 10 principais postagens, use a ordem decrescente:

foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(
                               redisKey, 0, 9, Order.Descending))
{
    Console.WriteLine(post);
}
Recuperar itens por intervalo de pontuação

Você também pode consultar itens com base nos limites de pontuação em vez de classificar:

foreach (var post in await cache.SortedSetRangeByScoreWithScoresAsync(
                               redisKey, 5000, 100000))
{
    Console.WriteLine(post);
}

Para impedir que um placar de líderes cresça indefinidamente, remova entradas antigas usando SortedSetRemoveRangeByRankAsync ou use chaves com escopo de tempo (por exemplo, placares de líderes diários ou semanais). Você pode atualizar pontuações atomicamente usando SortedSetIncrementAsync (ZINCRBY).

Estado da sessão de cache e saída HTML

Você pode usar o Redis Gerenciado do Azure para armazenar dados de estado da sessão e de cache de saída para aplicativos ASP.NET Core e ASP.NET. Quando você mantém dados de sessão e saída renderizada em um cache compartilhado baseado em Redis, aplicativos em execução em várias instâncias, como em Serviço de Aplicativo do Azure, AKS (Serviço de Kubernetes do Azure), Aplicativos de Contêiner do Azure, ou conjuntos de dimensionamento de máquinas virtuais, podem manter experiências consistentes do usuário sem a necessidade de afinidade de servidor.

Dica

Para obter melhor desempenho, implante seu aplicativo e a instância do Redis Gerenciado do Azure na mesma região do Azure.

ASP.NET Core

Os aplicativos ASP.NET Core usam a abstração IDistributedCache e o middleware de sessão. O Redis Gerenciado do Azure integra-se a IDistributedCache por meio do pacote Microsoft.Extensions.Caching.StackExchangeRedis.

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "<your-cache-name>.<region>.redis.azure.net:10000";
    options.InstanceName = "app-cache:";
});

builder.Services.AddSession();

O middleware de cache de saída do ASP.NET Core também pode usar o Redis como armazenamento de suporte distribuído, permitindo que os aplicativos compartilhem fragmentos ou páginas renderizadas entre todas as instâncias. Para obter mais informações, consulte ASP.NET Provedor de cache de saída principal para Redis.

Integração do .NET Aspire

Os aplicativos .NET Aspire podem usar o Aspire.Hosting.Azure.Redis pacote para declarar um recurso redis gerenciado do Azure no host do aplicativo. Os projetos de consumo recebem a configuração de conexão automaticamente por meio da injeção de dependência, o que elimina o gerenciamento manual de cadeia de conexão entre serviços.

// App host: declare the Azure Managed Redis resource
var cache = builder.AddAzureManagedRedis("cache");

builder.AddProject<Projects.ProductService>()
    .WithReference(cache);

Os serviços de consumo registram o cache distribuído da mesma forma que qualquer outro IDistributedCache provedor. Para obter mais informações, consulte Introdução à integração do Redis.

Alta disponibilidade, escalabilidade e particionamento

Cada instância do Redis Gerenciado do Azure usa replicação primária/réplica. O serviço monitora a integridade do nó e promove automaticamente uma réplica se o primário falhar. Como a replicação é assíncrona, uma pequena quantidade de dados gravados recentemente pode ser perdida durante um failover inesperado. Para obter as estratégias gerais por trás de replicação, failover e cache em camadas, consulte Implementar alta disponibilidade e escalabilidade e melhorar o desempenho anteriormente neste artigo.

Você pode combinar um cache local na memória com o Redis Gerenciado do Azure para reduzir a latência e fornecer um fallback se o cache compartilhado estiver temporariamente inacessível. O padrão Circuit-Breaker e o padrão Cache-Aside ajudam a gerenciar essa abordagem em camadas.

Para cargas de trabalho que excedem a capacidade de um único nó, o Redis Gerenciado do Azure dá suporte a dados de particionamento (fragmentação) em vários nós Redis. Com as duas políticas de clustering, os dados são automaticamente particionados entre os nós por meio de hash de chave para fragmento, failover automático e ressincronização, e reparticionamento online (expansão e redução de escala). O Redis Gerenciado do Azure dá suporte a duas políticas de clustering:

  • Política de Clustering do OSS (padrão): os clientes se comunicam diretamente com o fragmento apropriado e seguem a semântica do Cluster do Redis do OSS, incluindo redirecionamentos MOVED e ASK. Clientes com reconhecimento de cluster, como StackExchange.Redis, lidam com esses redirecionamentos automaticamente. Essa política minimiza a sobrecarga de roteamento.

  • Política de Clustering do Redis Enterprise: Um proxy fornece roteamento transparente por meio de um único endpoint. Os clientes não precisam implementar uma lógica compatível com cluster nem lidar com respostas MOVED/ASK. Essa política oferece uma integração de cliente mais simples, mas introduz uma pequena quantidade de sobrecarga de roteamento.

Azure Redis Gerenciado também dá suporte ao modo nonclustered, que usa um único par primário/réplica sem fragmentação. Esse modo é adequado para cargas de trabalho menores que não exigem expansão horizontal.

Note

Modelos de particionamento personalizados, como hash do lado do cliente ou proxies não Microsoft, normalmente são necessários apenas em implantações autogerenciadas do Redis em máquinas virtuais ou Kubernetes. O agrupamento do Redis Gerenciado do Azure manipula automaticamente o roteamento, a falha e a realocação de dados.

Replicação geográfica ativa

Para disponibilidade de várias regiões, o Azure Redis Gerenciado dá suporte à replicação geográfica ativa, que vincula instâncias entre regiões Azure em um único grupo de replicação. Cada instância pode lidar com leituras e gravações, e as alterações são sincronizadas automaticamente. Seu aplicativo é responsável por redirecionar o tráfego para uma instância íntegra durante uma falha regional. Para obter mais informações, consulte Configurar Replicação geográfica ativa.

Persistência de dados

Por padrão, os dados armazenados em cache no Redis Gerenciado do Azure são mantidos na memória e podem ser perdidos se um nó for reiniciado ou falhar. Para cargas de trabalho em que a recriação do cache do armazenamento de dados de origem seria lenta ou cara, o Redis Gerenciado do Azure dá suporte à persistência de dados opcional:

  • Instantâneos RDB (banco de dados Redis) criam capturas periódicas em um ponto no tempo, salvas em um disco gerenciado. O RDB tem um efeito de desempenho mínimo durante operações normais, mas os dados gravados desde o último instantâneo podem ser perdidos.

  • O AOF (arquivo somente de acréscimo) registra todas as operações de gravação em disco. AOF reduz a perda de dados potencial para aproximadamente um segundo de gravações, mas produz arquivos maiores e pode reduzir o desempenho de gravação.

Você pode usar o RDB e o AOF juntos. O Redis carrega o instantâneo RDB na inicialização e depois reproduz o log do AOF para uma recuperação quase completa.

Importante

A persistência melhora a durabilidade em relação a falhas de nó, mas não é um mecanismo de backup ou recuperação de desastre. Para dados críticos, sempre mantenha a cópia oficial no repositório de dados de origem e use o padrão Cache-Aside para preencher o cache novamente.

Para obter detalhes de configuração, consulte Configurar a persistência de dados.

Proteger dados armazenados em cache no Redis Gerenciado do Azure

As diretrizes em Proteger dados armazenados em cache descrevem as preocupações de controle de acesso e dados em trânsito. Azure Redis Gerenciado ajuda você a resolver essas preocupações das seguintes maneiras:

  • Use a autenticação da ID do Microsoft Entra como o mecanismo de controle de acesso primário e siga o princípio de privilégio mínimo ao conceder acesso.

  • Use pontos de extremidade privados para restringir o acesso à rede para que o tráfego não percorra a Internet pública.

  • Azure Redis Gerenciado criptografa dados em trânsito usando TLS e criptografa dados em repouso.

Considerações sobre serialização

Ao armazenar objetos .NET no Redis como valores de cadeia de caracteres, você precisa serializá-los. Ao escolher um formato de serialização, considere as compensações entre desempenho, interoperabilidade, controle de versão e tamanho da carga. Não há nenhum serializador mais rápido para todos os cenários. Os parâmetros de comparação são altamente dependentes do contexto e podem não refletir sua carga de trabalho real.

Se a camada Redis Gerenciada do Azure for compatível com RedisJSON, você poderá armazenar objetos como documentos JSON nativos e consultar campos individuais sem desserializar todo o valor:

public static class RedisJsonExtensions
{
    public static async Task<T?> GetAsync<T>(
        this IDatabase cache,
        string key,
        string path = "$")
    {
        var result = await cache.ExecuteAsync("JSON.GET", key, path);

        if (result.IsNull)
            return default;

        return JsonSerializer.Deserialize<T>(result!);
    }

    public static async Task SetAsync<T>(
        this IDatabase cache,
        string key,
        T value,
        TimeSpan? expiry = null,
        string path = "$")
    {
        var json = JsonSerializer.Serialize(value);

        // Store JSON document
        await cache.ExecuteAsync("JSON.SET", key, path, json);

        // Apply TTL if provided
        if (expiry.HasValue)
        {
            await cache.KeyExpireAsync(key, expiry);
        }
    }

    public static async Task<bool> ExpireAsync(
        this IDatabase cache,
        string key,
        TimeSpan expiry)
    {
        return await cache.KeyExpireAsync(key, expiry);
    }
}

Ao invés disso, ao serializar valores como cadeias de caracteres no Redis, as opções de formato comuns incluem:

  • JSON – Formato legível por humanos que tem amplo suporte multiplataforma. Não é o formato mais compacto, mas uma boa opção quando os itens armazenados em cache são retornados diretamente para clientes HTTP porque evita uma etapa extra de desserialização e reserialização.

  • MessagePack – Um formato binário compacto que não tem nenhum requisito de esquema. Produz cargas menores que JSON com menor sobrecarga de serialização.

  • Buffers de protocolo (protobuf) – um formato binário baseado em esquema que produz cargas compactas. Requer arquivos de .proto definição e uma etapa de compilação para gerar código específico do idioma.

  • BSON – Um formato binário que estende o JSON com mais tipos, como datas e dados binários brutos. Os payloads são comparáveis em tamanho ao JSON. Uma opção prática quando seu aplicativo já usa BSON em outro lugar, como com o MongoDB.

Os seguintes padrões podem ser relevantes ao implementar o cache em seus aplicativos:

  • Padrão Cache-Aside: Este padrão descreve como carregar dados sob demanda em um cache a partir de um armazenamento de dados. Ele também ajuda a manter a consistência entre os dados no cache e os dados no armazenamento de dados original.

  • Padrão de fragmentação: esse padrão fornece informações sobre como implementar o particionamento horizontal para ajudar a melhorar a escalabilidade ao armazenar e acessar grandes volumes de dados.