Compartilhar via


Diretrizes de cache

Redis Gerenciado pelo Azure

O caching é uma técnica comum que tem o objetivo de melhorar o desempenho e a escalabilidade de um sistema. Ele armazena em cache dados copiando temporariamente dados acessados com frequência para o armazenamento rápido localizado perto do aplicativo. Se esse armazenamento rápido de dados estiver mais próximo do aplicativo do que a fonte original, o caching poderá melhorar significativamente os tempos de resposta para aplicativos cliente ao fornecer dados com mais rapidez.

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:

  • Ele permanece relativamente estático.
  • É lento em comparação à velocidade do cache.
  • Está sujeito a um alto nível de contenção.
  • É muito distante quando a latência de rede pode fazer com que o acesso seja lento.

Cache em aplicativos distribuídos

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

  • Eles usam um cache privado, em que os dados são mantidos localmente no computador que está executando uma instância de um aplicativo ou serviço.
  • Eles usam um cache compartilhado, servindo como uma fonte comum que pode ser acessada por vários processos e computadores.

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

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 acessado diretamente pelo código que é executado nesse processo. Esse tipo de cache é rápido de acessar. Ele também pode fornecer um meio eficaz para armazenar quantidades modestas de dados estáticos. O tamanho de um cache normalmente é restringido pela quantidade de memória disponível no computador que hospeda o processo.

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. Esse processo é mais lento do que os dados mantidos na memória, mas ainda deve ser mais rápido e confiável 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 de aplicativo terá seu próprio cache independente segurando 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 na Figura 1.

Os resultados do uso de um cache na memória em instâncias diferentes de um aplicativo

Figura 1: usando um cache na memória em instâncias diferentes de um aplicativo.

Cache compartilhado

Se você usar um cache compartilhado, ele poderá ajudar a aliviar as preocupações de que os dados possam ser diferentes em cada cache, o que pode ocorrer com o cache na memória. O cache compartilhado garante que diferentes instâncias de aplicativo vejam a mesma exibição de dados armazenados em cache. Ele localiza o cache em um local separado, que normalmente é hospedado como parte de um serviço separado, conforme mostrado na Figura 2.

Os resultados do uso de um cache compartilhado

Figura 2: usando um cache compartilhado.

Um benefício importante da abordagem de cache compartilhado é a escalabilidade que ela fornece. 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 para cada instância do aplicativo.
  • O requisito para 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 com mais detalhes as considerações para criar 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 mantidos no cache.

Considere armazenar em cache dados lidos com frequência, mas modificados com pouca frequência (por exemplo, dados que têm uma proporção maior de operações de leitura do que operações de gravação). No entanto, não recomendamos que você use o cache como o repositório autoritativo de informações críticas. Em vez disso, verifique se todas as alterações que seu aplicativo não pode perder são sempre salvas 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

A chave para usar um cache efetivamente está na determinação dos dados mais apropriados para armazenar em cache e armazená-los em cache no momento apropriado. Os dados podem ser adicionados ao cache sob demanda na primeira vez em que são recuperados por um aplicativo. O aplicativo precisa buscar os dados apenas uma vez do armazenamento de dados e que o acesso subsequente possa ser atendido usando o cache.

Como alternativa, um cache pode ser parcial ou totalmente preenchido com dados com antecedência, normalmente quando o aplicativo é iniciado (uma abordagem conhecida como propagação). No entanto, pode não ser aconselhável implementar a propagação para um cache grande porque essa abordagem pode impor uma carga repentina e alta no armazenamento de dados original quando o aplicativo começa a ser executado.

Muitas vezes, uma análise dos padrões de uso pode ajudá-lo a decidir se deseja pré-preencher um cache totalmente ou parcialmente e escolher os dados para armazenar em cache. Por exemplo, você pode semear o cache com os dados de perfil de usuário estático para clientes que usam o aplicativo regularmente (talvez todos os dias), mas não para clientes que usam o aplicativo apenas uma vez por semana.

O cache normalmente funciona bem com dados que são 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. Alguns ou todos esses dados podem ser carregados 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 sejam up-todata. 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 a essa consideração (consulte a seção Cache de dados altamente dinâmicos mais adiante neste artigo para obter mais informações). 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 com 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 forem necessárias.

Recomendamos que você realize testes de desempenho e análise de uso para determinar se o pré-população ou o carregamento sob demanda do cache ou uma combinação de ambos é apropriado. A decisão deve ser baseada na volatilidade e no padrão de 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 propagar 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 mantidos em um cache. No entanto, recomendamos pensar no 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. Isso significa que, se o cache ficar indisponível, você minimizará a chance de perder dados.

Armazenar dados altamente dinâmicos em cache

Quando você armazena informações de alteração rápida em um armazenamento de dados persistente, ela 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 quase sempre estarã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 ter sido alterado.

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, os dados mantidos em um cache são uma cópia dos dados mantidos no armazenamento de dados original. Os dados no armazenamento de dados original podem ser alterados após serem armazenados em cache, fazendo com que os dados armazenados em cache fiquem obsoletos. Muitos sistemas de cache permitem configurar o cache para expirar os dados e reduzir o período para o qual os dados podem estar desatualizados.

Quando os dados armazenados em cache expiram, eles são removidos do cache e o aplicativo deve recuperar os dados do armazenamento de dados original (ele pode colocar as informações recém-buscadas de volta 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 estipular o período de expiração para objetos individuais ao armazená-los programaticamente no cache. Alguns caches permitem especificar o período de expiração como um valor absoluto ou como um valor deslizante que faz com que o item seja removido do cache se ele não for acessado dentro do 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.

Também é possível que o cache seja preenchido se os dados tiverem permissão para permanecer residentes por um longo tempo. Nesse caso, qualquer solicitação para adicionar novos itens ao cache pode fazer com que alguns itens sejam removidos à força em um processo conhecido como remoção. Os serviços de cache normalmente removem dados em uma base LRU (menos usada recentemente), mas você geralmente pode substituir essa política e impedir que os 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 ao cache falhará com uma exceção.

Algumas implementações de cache podem fornecer políticas de remoção adicionais. Há vários tipos de políticas de remoção. Elas incluem:

  • Uma política usada mais recentemente (na expectativa de que os dados não serão necessários novamente).
  • Uma política inicial (os dados mais antigos são removidos primeiro).
  • Uma política de remoção explícita com base em um evento disparado (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 geralmente são considerados fora dos auspícios 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.

Isso significa que é possível que um cliente que usa um cache mal configurado continue 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ê poderá fazer isso se um recurso for atualizado por uma alteração no URI desse 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.

Gerenciando simultaneidade em um cache

Os caches geralmente são projetados para serem compartilhados por várias instâncias de um aplicativo. Cada instância do aplicativo pode ler e modificar dados no cache. Consequentemente, 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, você pode adotar uma das duas abordagens para simultaneidade:

  • Optimistic. 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, a alteração poderá ser feita. Caso contrário, o aplicativo precisará 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.
  • Pessimistic. Quando ele recupera os dados, o aplicativo os bloqueia no cache para impedir que outra instância os altere. Esse processo garante que colisões não possam ocorrer, mas também podem 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; essa é a função do armazenamento de dados original do qual o cache é preenchido. O armazenamento de dados original é responsável por garantir a persistência dos 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 serviço que fornece 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, uma vez 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ão Cache-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 armazenamento de dados 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. A Figura 3 mostra essa estrutura.

Usando um cache privado local com um cache compartilhado

Figura 3: Usando um cache privado local com um cache compartilhado.

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 armazenados em 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 associada à 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 dinamicamente (e remover) nós e reequilibrar os dados entre partições. Essa abordagem pode envolver clustering, no qual a coleção de nós é apresentada a aplicativos cliente como um cache único e contínuo. 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 sobre possíveis estratégias de particionamento, consulte as diretrizes de particionamento de dados.

O clustering também pode aumentar a disponibilidade do cache. Se um nó falhar, o restante do cache ainda estará acessível. O clustering é frequentemente usado em conjunto com replicação e failover. Cada nó pode ser replicado e a réplica pode ser rapidamente colocada online 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. Isso permite que um aplicativo cliente empacote um grande volume de itens em uma única solicitação e reduz a sobrecarga associada à execução de um grande número de solicitações pequenas.

Cache 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), esse pode não ser o caso.

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 esse item de um cache, o que causa uma falha de cache, portanto, ele lê os dados do armazenamento de dados e os adiciona ao cache. No entanto, se o armazenamento de dados não tiver sido totalmente sincronizado com as outras réplicas, a instância do aplicativo poderá ler e preencher o cache com o valor antigo.

Para obter mais informações sobre como lidar com a consistência de dados, consulte a cartilha de consistência de dados.

Proteger dados armazenados em cache

Independentemente do serviço de cache usado, considere como proteger os dados mantidos no cache contra acesso não autorizado. Há duas preocupações principais:

  • 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 (leitura e gravação) que essas identidades têm permissão para executar.

Para reduzir a sobrecarga associada à leitura e gravação de dados, depois que uma identidade tiver recebido 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, poderá fazer uma das seguintes abordagens:

  • Divida o cache em partições (usando servidores de cache diferentes) e 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 e 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ó poderá 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. Para fazer isso, você 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 dentro da mesma organização que hospeda os aplicativos cliente, o isolamento da rede em si talvez não exija que você execute etapas adicionais. 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 SSL.

Considerações para implementar o cache no Azure

O Cache do Azure para Redis é uma implementação do cache Redis de software livre que é executado como um serviço em um datacenter do Azure. Ele fornece um serviço de cache que pode ser acessado de qualquer aplicativo do Azure, seja o aplicativo implementado como um serviço de nuvem, um site ou dentro de uma máquina virtual do Azure. Os caches podem ser compartilhados por aplicativos cliente que têm a chave de acesso apropriada.

O Cache do Azure para Redis é uma solução de cache de alto desempenho que fornece disponibilidade, escalabilidade e segurança. Normalmente, ele é executado como um serviço distribuído em um ou mais computadores dedicados. Ele tenta armazenar o máximo de informações possível na memória para garantir o acesso rápido. Essa arquitetura destina-se a fornecer baixa latência e alta taxa de transferência, reduzindo a necessidade de executar operações de E/S lentas.

O Cache do Azure para Redis é compatível com muitas das várias APIs usadas por aplicativos cliente. Se você tiver aplicativos existentes que já usam o Cache do Azure para Redis em execução local, o Cache do Azure para Redis fornecerá um caminho de migração rápida para o cache na nuvem.

Recursos do Redis

O Redis é mais do que um servidor de cache básico. Ele fornece um banco de dados distribuído na memória com um amplo conjunto de comandos que dá suporte a muitos cenários comuns. Elas são descritas posteriormente neste documento, na seção Usando o cache redis. Esta seção resume alguns dos principais recursos que o Redis fornece.

Redis como um banco de dados na memória

O Redis dá suporte a operações de leitura e gravação. No Redis, as gravações podem ser protegidas contra falhas do sistema por serem armazenadas periodicamente em um arquivo de instantâneo local ou em um arquivo de log somente acréscimo. Essa situação não é o caso em muitos caches, que devem ser considerados armazenamentos de dados transitórios.

Todas as gravações são assíncronas e não impedem os clientes de ler e gravar dados. Quando o Redis começa a ser executado, ele lê os dados do instantâneo ou do arquivo de log e os usa para construir o cache na memória. Para obter mais informações, consulte a persistência do Redis no site do Redis.

Note

O Redis não garante que todas as gravações sejam salvas se houver uma falha catastrófica, mas, na pior das hipóteses, você poderá perder apenas alguns segundos de dados. Lembre-se de que um cache não se destina a atuar como uma fonte de dados autoritativa e é responsabilidade dos aplicativos que usam o cache garantir que os dados críticos sejam salvos com êxito em um armazenamento de dados apropriado. Para obter mais informações, consulte o padrão Cache-aside.

Tipos de dados redis

O Redis é um repositório chave-valor, onde os valores podem conter tipos simples ou estruturas de dados complexas, como hashes, listas e conjuntos. Ele dá suporte a um conjunto de operações atômicas nesses tipos de dados. As chaves podem ser permanentes ou marcadas com um tempo de vida útil limitado, momento em que a chave e seu valor correspondente são automaticamente removidos do cache. Para obter mais informações sobre chaves e valores redis, visite a página Uma introdução aos tipos de dados e abstrações do Redis no site do Redis.

Replicação e clustering do Redis

O Redis dá suporte à replicação primária/subordinada para ajudar a garantir a disponibilidade e manter a taxa de transferência. As operações de gravação em um nó primário redis são replicadas para um ou mais nós subordinados. As operações de leitura podem ser atendidas pelo primário ou por qualquer um dos subordinados.

Se você tiver uma partição de rede, os subordinados poderão continuar a fornecer dados e, em seguida, ressincronizar de forma transparente com o primário quando a conexão for restabelecida. Para obter mais detalhes, visite a página Replicação no site do Redis.

O Redis também fornece clustering, que permite particionar dados de forma transparente em fragmentos entre servidores e espalhar a carga. Esse recurso melhora a escalabilidade, pois novos servidores Redis podem ser adicionados e os dados reparticionados à medida que o tamanho do cache aumenta.

Além disso, cada servidor no cluster pode ser replicado usando a replicação primária/subordinada. Isso garante a disponibilidade em cada nó no cluster. Para obter mais informações sobre clustering e fragmentação, visite a página do tutorial do cluster Redis no site do Redis.

Uso de memória redis

Um cache Redis tem um tamanho finito que depende dos recursos disponíveis no computador host. Ao configurar um servidor Redis, você pode especificar a quantidade máxima de memória que ele pode usar. Você também pode configurar uma chave em um cache Redis para ter um tempo de expiração, após o qual ela é removida automaticamente do cache. Esse recurso pode ajudar a impedir que o cache na memória seja preenchido com dados antigos ou obsoletos.

À medida que a memória é preenchida, o Redis pode remover automaticamente as chaves e seus valores com base em políticas diferentes. O padrão é menos usado recentemente (LRU), mas você também pode escolher outras opções, como remover chaves aleatoriamente ou desabilitar totalmente a remoção. Nesse caso, as tentativas de adicionar itens ao cache falharão se ele estiver cheio. Para obter mais informações, consulte Usar Redis como um cache LRU.

Transações e lotes do Redis

O Redis permite que um aplicativo cliente envie uma série de operações que leem e gravam dados no cache como uma transação atômica. Todos os comandos na transação têm a garantia de serem executados sequencialmente e nenhum comando emitido por outros clientes simultâneos é entrelaçado entre eles.

No entanto, essas transações não são verdadeiras, pois um banco de dados relacional as executaria. O processamento de transações consiste em dois estágios: o primeiro é quando os comandos são enfileirados e o segundo é quando os comandos são executados. Durante o estágio de enfileiramento de comando, os comandos que compõem a transação são enviados pelo cliente. Se algum tipo de erro ocorrer neste ponto (como um erro de sintaxe ou o número errado de parâmetros), o Redis se recusará a processar toda a transação e a descartará.

Durante a fase de execução, o Redis executa cada comando enfileirado em sequência. Se um comando falhar durante essa fase, o Redis continuará com o próximo comando enfileirado e não reverterá os efeitos de comandos que já foram executados. Essa forma simplificada de transação ajuda a manter o desempenho e evitar problemas de desempenho causados pela contenção.

O Redis implementa uma forma de bloqueio otimista para ajudar a manter a consistência. Para obter informações detalhadas sobre transações e bloqueio com o Redis, visite a página Transações no site do Redis.

O Redis também dá suporte ao envio em lotes não transacionais de solicitações. O protocolo Redis que os clientes usam para enviar comandos para um servidor Redis permite que um cliente envie uma série de operações como parte da mesma solicitação. Isso pode ajudar a reduzir a fragmentação de pacotes na rede. Quando o processo em lote é executado, cada comando é executado. Se qualquer um desses comandos estiver malformado, eles serão rejeitados (o que não acontece com uma transação), mas os comandos restantes serão executados. Não há também garantia sobre a ordem em que os comandos são executados no processo em lote.

Segurança do Redis

O Redis se concentra apenas no fornecimento de acesso rápido aos dados e foi projetado para ser executado dentro de um ambiente confiável que só pode ser acessado por clientes confiáveis. O Redis dá suporte a um modelo de segurança limitado com base na autenticação de senha. (É possível remover a autenticação completamente, embora não recomendamos isso.)

Todos os clientes autenticados compartilham a mesma senha global e têm acesso aos mesmos recursos. Se você precisar de uma segurança de entrada mais abrangente, deverá implementar sua própria camada de segurança na frente do servidor Redis e todas as solicitações de cliente deverão passar por essa camada adicional. O Redis não deve ser exposto diretamente a clientes não confiáveis ou não autenticados.

Você pode restringir o acesso a comandos desabilitando-os ou renomeando-os (e fornecendo apenas clientes privilegiados com os novos nomes).

O Redis não dá suporte diretamente a nenhuma forma de criptografia de dados, portanto, toda codificação deve ser executada por aplicativos cliente. Além disso, o Redis não fornece nenhuma forma de segurança de transporte. Se você precisar proteger os dados à medida que eles fluem pela rede, recomendamos implementar um proxy SSL.

Para obter mais informações, visite a página de segurança do Redis no site do Redis.

Note

O Cache do Azure para Redis fornece sua própria camada de segurança por meio da qual os clientes se conectam. Os servidores Redis subjacentes não são expostos à rede pública.

Cache Redis do Azure

O Cache do Azure para Redis fornece acesso aos servidores Redis hospedados em um datacenter do Azure. Ele atua como uma fachada que fornece controle de acesso e segurança. Você pode provisionar um cache usando o portal do Azure.

O portal fornece várias configurações predefinidas. Essas configurações variam de um cache de 53 GB em execução como um serviço dedicado que dá suporte a comunicações SSL (para privacidade) e replicação mestra/subordinada com um SLA (contrato de nível de serviço) de 99,9% disponibilidade, até um cache de 250 MB sem replicação (sem garantias de disponibilidade) em execução no hardware compartilhado.

Usando o portal do Azure, você também pode configurar a política de remoção do cache e controlar o acesso ao cache adicionando usuários às funções fornecidas. Essas funções, que definem as operações que os membros podem executar, incluem Proprietário, Colaborador e Leitor. Por exemplo, os membros da função Proprietário têm controle total sobre o cache (incluindo segurança) e seu conteúdo, os membros da função Colaborador podem ler e gravar informações no cache e os membros da função Leitor só podem recuperar dados do cache.

A maioria das tarefas administrativas é executada por meio do portal do Azure. Por esse motivo, muitos dos comandos administrativos disponíveis na versão padrão do Redis não estão disponíveis, incluindo a capacidade de modificar a configuração programaticamente, desligar o servidor Redis, configurar subordinados adicionais ou salvar dados em disco à força.

O portal do Azure inclui uma exibição gráfica conveniente que permite monitorar o desempenho do cache. Por exemplo, você pode exibir o número de conexões que estão sendo feitas, o número de solicitações sendo executadas, o volume de leituras e gravações e o número de acertos de cache versus erros de cache. Usando essas informações, você pode determinar a eficácia do cache e, se necessário, alternar para uma configuração diferente ou alterar a política de remoção.

Além disso, você pode criar alertas que enviam mensagens de email para um administrador se uma ou mais métricas críticas estiverem fora de um intervalo esperado. Por exemplo, talvez você queira alertar um administrador se o número de erros de cache exceder um valor especificado na última hora, pois isso significa que o cache pode ser muito pequeno ou os dados podem estar sendo removidos muito rapidamente.

Você também pode monitorar a CPU, a memória e o uso da rede para o cache.

Para obter mais informações e exemplos que mostram como criar e configurar um Cache do Azure para Redis, visite a página Volta ao redor do Cache do Azure para Redis no blog do Azure.

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

Se você criar ASP.NET aplicativos Web executados usando funções Web do Azure, você poderá salvar informações de estado de sessão e saída HTML em um Cache do Azure para Redis. O provedor de estado de sessão do Cache do Azure para Redis permite compartilhar informações de sessão entre instâncias diferentes de um aplicativo Web ASP.NET e é muito útil em situações de farm da Web em que a afinidade cliente-servidor não está disponível e armazenar dados de sessão em cache na memória não seria apropriado.

O uso do provedor de estado de sessão com o Cache do Azure para Redis oferece vários benefícios, incluindo:

  • Compartilhando o estado da sessão com um grande número de instâncias de ASP.NET aplicativos Web.
  • Fornecendo escalabilidade aprimorada.
  • Suporte ao acesso controlado e simultâneo aos mesmos dados de estado de sessão para vários leitores e um único gravador.
  • Usar a compactação para salvar memória e melhorar o desempenho da rede.

Para obter mais informações, consulte ASP.NET provedor de estado de sessão para o Cache do Azure para Redis.

Note

Não use o provedor de estado de sessão para o Cache do Azure para Redis com aplicativos ASP.NET que são executados fora do ambiente do Azure. A latência de acessar o cache de fora do Azure pode eliminar os benefícios de desempenho do cache de dados.

Da mesma forma, o provedor de cache de saída do Cache do Azure para Redis permite que você salve as respostas HTTP geradas por um aplicativo Web ASP.NET. Usar o provedor de cache de saída com o Cache do Azure para Redis pode melhorar os tempos de resposta de aplicativos que renderizam saída HTML complexa. Instâncias de aplicativo que geram respostas semelhantes podem usar os fragmentos de saída compartilhados no cache em vez de gerar essa saída HTML de novo. Para obter mais informações, consulte ASP.NET provedor de cache de saída para o Cache do Azure para Redis.

Criando um cache Redis personalizado

O Cache do Azure para Redis atua como uma fachada para os servidores Redis subjacentes. Se você precisar de uma configuração avançada que não seja coberta pelo cache Redis do Azure (como um cache maior que 53 GB), você poderá criar e hospedar seus próprios servidores Redis usando máquinas virtuais do Azure.

Esse é um processo potencialmente complexo porque talvez seja necessário criar várias VMs para atuar como nós primários e subordinados se quiser implementar a replicação. Além disso, se você quiser criar um cluster, precisará de várias primárias e servidores subordinados. Uma topologia de replicação clusterizado mínima que fornece um alto grau de disponibilidade e escalabilidade compreende pelo menos seis VMs organizadas como três pares de servidores primários/subordinados (um cluster deve conter pelo menos três nós primários).

Cada par primário/subordinado deve estar localizado próximo para minimizar a latência. No entanto, cada conjunto de pares pode estar em execução em datacenters diferentes do Azure localizados em regiões diferentes, se você quiser localizar dados armazenados em cache perto dos aplicativos que são mais propensos a usá-los. Para obter um exemplo de compilação e configuração de um nó Redis em execução como uma VM do Azure, consulte Executar o Redis em uma VM Do CentOS Linux no Azure.

Note

Se você implementar seu próprio cache Redis dessa forma, será responsável por monitorar, gerenciar e proteger o serviço.

Particionando um cache Redis

Particionar o cache envolve dividir o cache em vários computadores. Essa estrutura oferece várias vantagens em relação ao uso de um único servidor de cache, incluindo:

  • Criar um cache muito maior do que pode ser armazenado em um único servidor.
  • Distribuindo dados entre servidores, melhorando a disponibilidade. Se um servidor falhar ou se tornar inacessível, os dados que ele contém não estarão disponíveis, mas os dados nos servidores restantes ainda poderão ser acessados. Para um cache, isso não é crucial porque os dados armazenados em cache são apenas uma cópia transitória dos dados mantidos em um banco de dados. Em vez disso, os dados armazenados em cache em um servidor que se torna inacessível podem ser armazenados em cache em um servidor diferente.
  • Espalhando a carga entre servidores, o que melhora o desempenho e a escalabilidade.
  • Localização geográfica de dados próximos aos usuários que os acessam, reduzindo assim a latência.

Para um cache, a forma mais comum de particionamento é a fragmentação. Nessa estratégia, cada partição (ou fragmento) é um cache Redis por si só. Os dados são direcionados para uma partição específica usando a lógica de fragmentação, que pode usar várias abordagens para distribuir os dados. O padrão sharding fornece mais informações sobre como implementar a fragmentação.

Para implementar o particionamento em um cache Redis, você pode adotar uma das seguintes abordagens:

  • Roteamento de consulta do lado do servidor. Nessa técnica, um aplicativo cliente envia uma solicitação para qualquer um dos servidores Redis que compõem o cache (provavelmente o servidor mais próximo). Cada servidor Redis armazena metadados que descrevem a partição que ele contém e também contém informações sobre quais partições estão localizadas em outros servidores. O servidor Redis examina a solicitação do cliente. Se puder ser resolvido localmente, ele executará a operação solicitada. Caso contrário, ele encaminhará a solicitação para o servidor apropriado. Esse modelo é implementado pelo clustering redis e é descrito com mais detalhes na página do tutorial do cluster Redis no site do Redis. O clustering redis é transparente para aplicativos cliente e servidores Redis adicionais podem ser adicionados ao cluster (e os dados reparticionados) sem exigir que você reconfigure os clientes.
  • Particionamento do lado do cliente. Nesse modelo, o aplicativo cliente contém lógica (possivelmente na forma de uma biblioteca) que roteia solicitações para o servidor Redis apropriado. Essa abordagem pode ser usada com o Cache do Azure para Redis. Crie vários Caches do Azure para Redis (um para cada partição de dados) e implemente a lógica do lado do cliente que roteia as solicitações para o cache correto. Se o esquema de particionamento for alterado (se o Cache do Azure adicional para Redis for criado, por exemplo), os aplicativos cliente poderão precisar ser reconfigurados.
  • Particionamento assistido por proxy. Nesse esquema, os aplicativos cliente enviam solicitações para um serviço de proxy intermediário que entende como os dados são particionados e, em seguida, roteia a solicitação para o servidor Redis apropriado. Essa abordagem também pode ser usada com o Cache do Azure para Redis; o serviço proxy pode ser implementado como um serviço de nuvem do Azure. Essa abordagem requer um nível adicional de complexidade para implementar o serviço, e as solicitações podem levar mais tempo para serem executadas do que o uso do particionamento do lado do cliente.

O particionamento de página: como dividir dados entre várias instâncias do Redis no site do Redis fornece mais informações sobre como implementar o particionamento com o Redis.

Implementar aplicativos cliente do cache Redis

O Redis dá suporte a aplicativos cliente escritos em várias linguagens de programação. Se você criar novos aplicativos usando o .NET Framework, recomendamos que você use a biblioteca de clientes StackExchange.Redis. Essa biblioteca fornece um modelo de objeto do .NET Framework que abstrai os detalhes para se conectar a um servidor Redis, enviar comandos e receber respostas. Ele está disponível no Visual Studio como um pacote NuGet. Você pode usar essa mesma biblioteca para se conectar a um Cache do Azure para Redis ou a um cache Redis personalizado hospedado em uma VM.

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. A mesma conexão pode ser usada por vários threads simultâneos. Não se reconecte e desconecte sempre que você executar uma operação redis, pois isso pode prejudicar o desempenho.

Você pode especificar os parâmetros de conexão, como o endereço do host Redis e a senha. Se você usar o Cache do Azure para Redis, a senha será a chave primária ou secundária gerada para o Cache do Azure para Redis usando o portal do Azure.

Depois de se conectar ao servidor Redis, você pode obter um identificador no Banco de Dados Redis que atua como o cache. A conexão Redis fornece o GetDatabase método para fazer isso. Em seguida, você pode recuperar itens do cache e armazenar dados no cache usando os métodos e StringGet os StringSet métodos. Esses métodos esperam uma chave como um parâmetro e retornam o item no cache que tem um valor correspondente (StringGet) ou adicionam o item ao cache com essa chave (StringSet).

Dependendo do local do servidor Redis, muitas operações podem incorrer em alguma latência enquanto uma solicitação é transmitida para o servidor e uma resposta é retornada ao cliente. A biblioteca StackExchange fornece versões assíncronas de muitos dos métodos expostos para ajudar os aplicativos cliente a permanecerem responsivos. Esses métodos dão suporte ao padrão assíncrono baseado em tarefa no .NET Framework.

O snippet de código a seguir mostra um método chamado RetrieveItem. Ele ilustra uma implementação do padrão cache-aside com base no Redis e na biblioteca StackExchange. O método usa um valor de chave de cadeia de caracteres e tenta recuperar o item correspondente do cache Redis chamando o StringGetAsync método (a versão assíncrona de StringGet).

Se o item não for encontrado, ele será obtido da fonte de dados subjacente usando o GetItemFromDataSourceAsync método (que é um método local e não faz parte da biblioteca StackExchange). Em seguida, ele é adicionado ao cache usando o StringSetAsync método para que ele possa ser recuperado mais rapidamente na próxima vez.

// Connect to the Azure Redis cache
ConfigurationOptions config = new ConfigurationOptions();
config.EndPoints.Add("<your DNS name>.redis.cache.windows.net");
config.Password = "<Redis cache key from management portal>";
ConnectionMultiplexer redisHostConnection = ConnectionMultiplexer.Connect(config);
IDatabase cache = redisHostConnection.GetDatabase();
...
private async Task<string> RetrieveItem(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 == null)
    {
        itemValue = await GetItemFromDataSourceAsync(itemKey);
        await cache.StringSetAsync(itemKey, itemValue);
    }

    // Return the item
    return itemValue;
}

Os StringGet métodos e StringSet os métodos não estão restritos à recuperação ou ao armazenamento de valores de cadeia de caracteres. Eles podem pegar qualquer item serializado como uma matriz de bytes. Se você precisar salvar um objeto .NET, poderá serializá-lo como um fluxo de bytes e usar o StringSet método para gravá-lo no cache.

Da mesma forma, você pode ler um objeto do cache usando o método e desserializando-o StringGet como um objeto .NET. O código a seguir mostra um conjunto de métodos de extensão para a interface IDatabase (o GetDatabase método de uma conexão Redis retorna um IDatabase objeto) e algum código de exemplo que usa esses métodos para ler e gravar um BlogPost objeto no cache:

public static class RedisCacheExtensions
{
    public static async Task<T> GetAsync<T>(this IDatabase cache, string key)
    {
        return Deserialize<T>(await cache.StringGetAsync(key));
    }

    public static async Task<object> GetAsync(this IDatabase cache, string key)
    {
        return Deserialize<object>(await cache.StringGetAsync(key));
    }

    public static async Task SetAsync(this IDatabase cache, string key, object value)
    {
        await cache.StringSetAsync(key, Serialize(value));
    }

    static byte[] Serialize(object o)
    {
        byte[] objectDataAsStream = null;

        if (o != null)
        {
            var jsonString = JsonSerializer.Serialize(o);
            objectDataAsStream = Encoding.ASCII.GetBytes(jsonString);
        }

        return objectDataAsStream;
    }

    static T Deserialize<T>(byte[] stream)
    {
        T result = default(T);

        if (stream != null)
        {
            var jsonString = Encoding.ASCII.GetString(stream);
            result = JsonSerializer.Deserialize<T>(jsonString);
        }

        return result;
    }
}

O código a seguir ilustra um método chamado RetrieveBlogPost que usa esses métodos de extensão para ler e gravar um objeto serializável BlogPost no cache seguindo o padrão cache-aside:

// The BlogPost type
public class BlogPost
{
    private HashSet<string> tags;

    public BlogPost(int id, string title, int score, IEnumerable<string> tags)
    {
        this.Id = id;
        this.Title = title;
        this.Score = score;
        this.tags = new HashSet<string>(tags);
    }

    public int Id { get; set; }
    public string Title { get; set; }
    public int Score { get; set; }
    public ICollection<string> Tags => this.tags;
}
...
private async Task<BlogPost> RetrieveBlogPost(string blogPostKey)
{
    BlogPost blogPost = await cache.GetAsync<BlogPost>(blogPostKey);
    if (blogPost == null)
    {
        blogPost = await GetBlogPostFromDataSourceAsync(blogPostKey);
        await cache.SetAsync(blogPostKey, blogPost);
    }

    return blogPost;
}

O Redis dá suporte à pipelining de comando se um aplicativo cliente enviar várias solicitações assíncronas. O Redis pode multiplexar as solicitações usando a mesma conexão em vez de receber e responder a comandos em uma sequência estrita.

Essa abordagem ajuda a reduzir a latência fazendo uso mais eficiente da rede. O snippet de código a seguir mostra um exemplo que recupera os detalhes de dois clientes simultaneamente. O código envia duas solicitações e, em seguida, executa algum outro processamento (não mostrado) antes de aguardar para receber os resultados. O Wait método do objeto de cache é semelhante ao método .NET Framework Task.Wait :

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
var task1 = cache.StringGetAsync("customer:1");
var task2 = cache.StringGetAsync("customer:2");
...
var customer1 = cache.Wait(task1);
var customer2 = cache.Wait(task2);

Para obter mais informações sobre como escrever aplicativos cliente que podem usar o Cache do Azure para Redis, consulte a documentação do Cache do Azure para Redis. Mais informações também estão disponíveis em StackExchange.Redis.

A página Pipelines e multiplexers no mesmo site fornece mais informações sobre operações assíncronas e pipelining com Redis e a biblioteca StackExchange.

Usando o cache redis

O uso mais simples do Redis para armazenar em cache as preocupações são pares chave-valor em que o valor é uma cadeia de caracteres não interpretada de comprimento arbitrário que pode conter quaisquer dados binários. (É essencialmente uma matriz de bytes que pode ser tratada como uma cadeia de caracteres). Esse cenário foi ilustrado na seção Implementar aplicativos cliente do Cache Redis anteriormente neste artigo.

As chaves também contêm dados não interpretados, para que você possa usar qualquer informação binária como a chave. Quanto mais tempo a chave for, no entanto, mais espaço levará para armazenar e mais tempo levará para executar operações de pesquisa. Para usabilidade e facilidade de manutenção, projete seu keyspace com cuidado e use chaves significativas (mas não detalhadas).

Por exemplo, use chaves estruturadas como customer:100 (em vez de apenas 100) para representar a chave para o cliente com a ID 100. Esse esquema permite que você distingue entre valores que armazenam diferentes tipos de dados. Por exemplo, você também pode usar a chave orders:100 para representar a chave da ordem com a ID 100.

Além das cadeias de caracteres binárias unidimensionais, um valor em um par chave-valor Redis também pode conter informações mais estruturadas, incluindo listas, conjuntos (classificados e não classificados) e hashes. O Redis fornece um conjunto de comandos abrangente que pode manipular esses tipos e muitos desses comandos estão disponíveis para aplicativos do .NET Framework por meio de uma biblioteca de clientes, como o StackExchange. A página Uma introdução aos tipos de dados e abstrações do Redis no site do Redis fornece uma visão geral mais detalhada desses tipos e dos comandos que você pode usar para manipulá-los.

Esta seção resume alguns casos de uso comuns para esses tipos de dados e comandos.

Executar operações atômicas e em lotes

O Redis dá suporte a uma série de operações de get-and-set atômicas em valores de cadeia de caracteres. Essas operações removem as possíveis condições de corrida que podem ocorrer quando você usa comandos GET e SET separados. As operações disponíveis incluem:

  • INCR, INCRBY, DECRe DECRBY, que executam operações de incremento atômico e decremento em valores de dados numéricos inteiros. A biblioteca StackExchange fornece versões sobrecarregadas e IDatabase.StringIncrementAsyncIDatabase.StringDecrementAsync métodos para executar essas operações e retornar o valor resultante armazenado no cache. O snippet de código a seguir ilustra como usar esses métodos:

    ConnectionMultiplexer redisHostConnection = ...;
    IDatabase cache = redisHostConnection.GetDatabase();
    ...
    await cache.StringSetAsync("data:counter", 99);
    ...
    long oldValue = await cache.StringIncrementAsync("data:counter");
    // Increment by 1 (the default)
    // oldValue should be 100
    
    long newValue = await cache.StringDecrementAsync("data:counter", 50);
    // Decrement by 50
    // newValue should be 50
    
  • GETSET, que recupera o valor associado a uma chave e altera-o para um novo valor. A biblioteca StackExchange disponibiliza essa operação por meio do IDatabase.StringGetSetAsync método. O snippet de código a seguir mostra um exemplo desse método. Esse código retorna o valor atual associado à chave "data:counter" do exemplo anterior. Em seguida, ele redefine o valor dessa chave de volta para zero, tudo como parte da mesma operação:

    ConnectionMultiplexer redisHostConnection = ...;
    IDatabase cache = redisHostConnection.GetDatabase();
    ...
    string oldValue = await cache.StringGetSetAsync("data:counter", 0);
    
  • MGET e MSET, que podem retornar ou alterar um conjunto de valores de cadeia de caracteres como uma única operação. Os IDatabase.StringGetAsync métodos e os IDatabase.StringSetAsync métodos são sobrecarregados para dar suporte a essa funcionalidade, conforme mostrado no exemplo a seguir:

    ConnectionMultiplexer redisHostConnection = ...;
    IDatabase cache = redisHostConnection.GetDatabase();
    ...
    // Create a list of key-value pairs
    var keysAndValues =
        new List<KeyValuePair<RedisKey, RedisValue>>()
        {
            new KeyValuePair<RedisKey, RedisValue>("data:key1", "value1"),
            new KeyValuePair<RedisKey, RedisValue>("data:key99", "value2"),
            new KeyValuePair<RedisKey, RedisValue>("data:key322", "value3")
        };
    
    // Store the list of key-value pairs in the cache
    cache.StringSet(keysAndValues.ToArray());
    ...
    // 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 = cache.StringGet(keys);
    
    

Você também pode combinar várias operações em uma única transação redis, conforme descrito na seção transações e lotes do Redis anteriormente neste artigo. 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 semelhantes aos 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 que incrementa e decrementa dois contadores como parte da mesma transação:

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
ITransaction transaction = cache.CreateTransaction();
var tx1 = transaction.StringIncrementAsync("data:counter1");
var tx2 = transaction.StringDecrementAsync("data:counter2");
bool result = transaction.Execute();
Console.WriteLine("Transaction {0}", result ? "succeeded" : "failed");
Console.WriteLine("Result of increment: {0}", tx1.Result);
Console.WriteLine("Result of decrement: {0}", tx2.Result);

Lembre-se de que 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 tiverem sido 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, você deverá buscar os resultados do comando usando a propriedade Resultado da tarefa correspondente, conforme mostrado no exemplo anterior. A leitura da propriedade Resultado bloqueará o thread de chamada até que a tarefa seja concluída.

Para obter mais informações, consulte Transações no Redis.

Ao executar operações em lote, você pode usar a IBatch interface da biblioteca StackExchange. Essa interface fornece acesso a um conjunto de métodos semelhantes aos acessados pela IDatabase interface, exceto que todos os métodos são assíncronos.

Crie um IBatch objeto usando o IDatabase.CreateBatch método e execute o lote usando o IBatch.Execute método, conforme mostrado no exemplo a seguir. Esse código define um valor de cadeia de caracteres, incrementa e decrementa os mesmos contadores usados no exemplo anterior e exibe os resultados:

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
IBatch batch = cache.CreateBatch();
batch.StringSetAsync("data:key1", 11);
var t1 = batch.StringIncrementAsync("data:counter1");
var t2 = batch.StringDecrementAsync("data:counter2");
batch.Execute();
Console.WriteLine("{0}", t1.Result);
Console.WriteLine("{0}", t2.Result);

É importante entender que, ao contrário de uma transação, se um comando em um lote falhar porque ele está malformado, os outros comandos ainda poderão ser executados. O IBatch.Execute método não retorna nenhuma indicação de êxito ou falha.

Executar operações de cache de fogo e esquecer

O Redis dá suporte a operações de fogo e de esquecer usando sinalizadores de comando. Nessa situação, o cliente inicia uma operação, mas não tem interesse no resultado e não aguarda a conclusão do comando. O exemplo a seguir mostra como executar o comando INCR como uma operação de envio sem confirmação:

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
await cache.StringSetAsync("data:key1", 99);
...
cache.StringIncrement("data:key1", flags: CommandFlags.FireAndForget);

Especificar chaves expirando automaticamente

Ao armazenar um item em um cache Redis, você pode especificar um tempo limite após o qual o item é removido automaticamente do cache. 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 usando o 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:

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// 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 a null
TimeSpan? expiry = cache.KeyTimeToLive("data:key1");

Você também pode definir a hora de expiração para uma data e hora específicas usando o comando EXPIRE, que está disponível na biblioteca StackExchange como o KeyExpireAsync método:

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// Add a key with an expiration date of midnight on 1st January 2015
await cache.StringSetAsync("data:key1", 99);
await cache.KeyExpireAsync("data:key1",
    new DateTime(2015, 1, 1, 0, 0, 0, DateTimeKind.Utc));
...

Tip

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

Usar marcas para correlacionar entre itens armazenados em cache

Um conjunto redis é uma coleção de vários itens que compartilham uma única chave. Você pode criar um conjunto usando o comando SADD. Você pode recuperar os itens em um conjunto usando o comando SMEMBERS. A biblioteca StackExchange implementa o comando SADD com o IDatabase.SetAddAsync método e o comando SMEMBERS com o IDatabase.SetMembersAsync método.

Você também pode combinar conjuntos existentes para criar novos conjuntos usando os comandos SDIFF (definir diferença), SINTER (definir interseção) e SUNION (definir união). A biblioteca StackExchange unifica essas operações no IDatabase.SetCombineAsync método. O primeiro parâmetro para esse método especifica a operação de conjunto a ser executada.

Os snippets de código a seguir mostram como os conjuntos podem ser úteis para armazenar e recuperar rapidamente coleções de itens relacionados. Esse código usa o BlogPost tipo descrito na seção Implementar Aplicativos Cliente do Cache Redis anteriormente neste artigo.

Um BlogPost objeto contém quatro campos: uma ID, um título, uma pontuação de classificação e uma coleção de marcas. O primeiro snippet de código mostra os dados de exemplo usados para preencher uma lista de BlogPost objetos em C#:

List<string[]> tags = new List<string[]>
{
    new[] { "iot","csharp" },
    new[] { "iot","azure","csharp" },
    new[] { "csharp","git","big data" },
    new[] { "iot","git","database" },
    new[] { "database","git" },
    new[] { "csharp","database" },
    new[] { "iot" },
    new[] { "iot","database","git" },
    new[] { "azure","database","big data","git","csharp" },
    new[] { "azure" }
};

List<BlogPost> posts = new List<BlogPost>();
int blogKey = 0;
int numberOfPosts = 20;
Random random = new Random();
for (int i = 0; i < numberOfPosts; i++)
{
    blogKey++;
    posts.Add(new BlogPost(
        blogKey,                  // Blog post ID
        string.Format(CultureInfo.InvariantCulture, "Blog Post #{0}",
            blogKey),             // Blog post title
        random.Next(100, 10000),  // Ranking score
        tags[i % tags.Count]));   // Tags--assigned from a collection
                                  // in the tags list
}

Você pode armazenar as marcas de cada BlogPost objeto como um conjunto em um cache Redis e associar cada conjunto à ID do BlogPost. Isso permite que um aplicativo localize rapidamente todas as marcas que pertencem a uma postagem de blog específica. Para habilitar a pesquisa na direção oposta e encontrar todas as postagens de blog que compartilham uma marca específica, você pode criar outro conjunto que contém as postagens de blog que fazem referência à ID da marca na chave:

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// Tags are easily represented as Redis Sets
foreach (BlogPost post in posts)
{
    string redisKey = string.Format(CultureInfo.InvariantCulture,
        "blog:posts:{0}:tags", post.Id);
    // Add tags to the blog post in Redis
    await cache.SetAddAsync(
        redisKey, post.Tags.Select(s => (RedisValue)s).ToArray());

    // Now do the inverse so we can figure out which blog posts have a given tag
    foreach (var tag in post.Tags)
    {
        await cache.SetAddAsync(string.Format(CultureInfo.InvariantCulture,
            "tag:{0}:blog:posts", tag), post.Id);
    }
}

Essas estruturas permitem que você execute muitas consultas comuns com muita eficiência. Por exemplo, você pode encontrar e exibir todas as marcas para a postagem de blog 1 desta forma:

// Show the tags for blog post #1
foreach (var value in await cache.SetMembersAsync("blog:posts:1:tags"))
{
    Console.WriteLine(value);
}

Você pode encontrar todas as marcas comuns à postagem de blog 1 e postagem de blog 2 executando uma operação de interseção definida, da seguinte maneira:

// Show the tags in common for blog posts #1 and #2
foreach (var value in await cache.SetCombineAsync(SetOperation.Intersect, new RedisKey[]
    { "blog:posts:1:tags", "blog:posts:2:tags" }))
{
    Console.WriteLine(value);
}

E você pode encontrar todas as postagens de blog que contêm uma marca específica:

// Show the ids of the blog posts that have the tag "iot".
foreach (var value in await cache.SetMembersAsync("tag:iot:blog:posts"))
{
    Console.WriteLine(value);
}

Localizar itens acessados recentemente

Uma tarefa comum necessária para muitos aplicativos é localizar os itens acessados mais recentemente. Por exemplo, um site de blogs pode querer exibir informações sobre as postagens de blog lidas mais recentemente.

Você pode implementar essa funcionalidade usando uma lista do Redis. Uma lista redis contém vários itens que compartilham a mesma chave. A lista atua como uma fila de duas pontas. Você pode enviar itens por push para qualquer extremidade da lista usando os comandos LPUSH (push esquerdo) e RPUSH (push direito). Você pode recuperar itens de qualquer extremidade da lista usando os comandos LPOP e RPOP. Você também pode retornar um conjunto de elementos usando os comandos LRANGE e RRANGE.

Os snippets de código a seguir mostram como você pode executar essas operações usando a biblioteca StackExchange. Esse código usa o BlogPost tipo dos exemplos anteriores. Como uma postagem no blog é lida por um usuário, o IDatabase.ListLeftPushAsync método envia o título da postagem do blog para uma lista associada à chave "blog:recent_posts" no cache Redis.

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
string redisKey = "blog:recent_posts";
BlogPost blogPost = ...; // Reference to the blog post that has just been read
await cache.ListLeftPushAsync(
    redisKey, blogPost.Title); // Push the blog post onto the list

À medida que mais postagens de blog são lidas, seus títulos são enviados por push para a mesma lista. A lista é ordenada pela sequência na qual os títulos são adicionados. As postagens de blog lidas mais recentemente estão no final esquerdo da lista. (Se a mesma postagem de blog for lida mais de uma vez, ela terá várias entradas na lista.)

Você pode exibir os títulos das postagens lidas mais recentemente usando o IDatabase.ListRange método. Esse método usa a chave que contém a lista, um ponto de partida e um ponto final. O código a seguir recupera os títulos das 10 postagens de blog (itens de 0 a 9) no final mais à esquerda da lista:

// Show latest ten posts
foreach (string postTitle in await cache.ListRangeAsync(redisKey, 0, 9))
{
    Console.WriteLine(postTitle);
}

O ListRangeAsync método não remove itens da lista. Para fazer isso, você pode usar os métodos e IDatabase.ListLeftPopAsync os IDatabase.ListRightPopAsync métodos.

Para impedir que a lista cresça indefinidamente, você pode reduzir periodicamente os itens cortando a lista. O snippet de código a seguir mostra como remover todos, exceto os cinco itens mais à esquerda da lista:

await cache.ListTrimAsync(redisKey, 0, 5);

Implementar um quadro de líderes

Por padrão, os itens em um conjunto não são mantidos em nenhuma ordem específica. Você pode criar um conjunto ordenado usando o comando ZADD (o IDatabase.SortedSetAdd método na biblioteca StackExchange). Os itens são ordenados usando um valor numérico chamado pontuação, que é fornecido como um parâmetro para o comando.

O snippet de código a seguir adiciona o título de uma postagem de blog a uma lista ordenada. Neste exemplo, cada postagem de blog também tem um campo de pontuação que contém a classificação da postagem no blog.

ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
string redisKey = "blog:post_rankings";
BlogPost blogPost = ...; // Reference to a blog post that has just been rated
await cache.SortedSetAddAsync(redisKey, blogPost.Title, blogPost.Score);

Você pode recuperar os títulos de postagem do blog e as pontuações em ordem de pontuação crescente usando o IDatabase.SortedSetRangeByRankWithScores método:

foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(redisKey))
{
    Console.WriteLine(post);
}

Note

A biblioteca StackExchange também fornece o IDatabase.SortedSetRangeByRankAsync método, que retorna os dados na ordem de pontuação, mas não retorna as pontuações.

Você também pode recuperar itens em ordem decrescente de pontuações e limitar o número de itens retornados fornecendo parâmetros adicionais ao IDatabase.SortedSetRangeByRankWithScoresAsync método. O exemplo a seguir exibe os títulos e as pontuações das 10 principais postagens de blog classificadas:

foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(
                               redisKey, 0, 9, Order.Descending))
{
    Console.WriteLine(post);
}

O próximo exemplo usa o IDatabase.SortedSetRangeByScoreWithScoresAsync método, que você pode usar para limitar os itens que são retornados para aqueles que se enquadram em um determinado intervalo de pontuação:

// Blog posts with scores between 5000 and 100000
foreach (var post in await cache.SortedSetRangeByScoreWithScoresAsync(
                               redisKey, 5000, 100000))
{
    Console.WriteLine(post);
}

Mensagem usando canais

Além de atuar como um cache de dados, um servidor Redis fornece mensagens por meio de um mecanismo de editor/assinante de alto desempenho. Os aplicativos cliente podem assinar um canal e outros aplicativos ou serviços podem publicar mensagens no canal. Aplicativos assinantes podem então receber essas mensagens e processá-las.

O Redis fornece o comando SUBSCRIBE para aplicativos cliente a serem usados para assinar canais. Esse comando espera o nome de um ou mais canais nos quais o aplicativo aceita mensagens. A biblioteca StackExchange inclui a ISubscription interface, que permite que um aplicativo .NET Framework assine e publique em canais.

Você cria um ISubscription objeto usando o GetSubscriber método da conexão com o servidor Redis. Em seguida, você escuta mensagens em um canal usando o SubscribeAsync método deste objeto. O exemplo de código a seguir mostra como assinar um canal chamado "messages:blogPosts":

ConnectionMultiplexer redisHostConnection = ...;
ISubscriber subscriber = redisHostConnection.GetSubscriber();
...
await subscriber.SubscribeAsync("messages:blogPosts", (channel, message) => Console.WriteLine("Title is: {0}", message));

O primeiro parâmetro para o Subscribe método é o nome do canal. Esse nome segue as mesmas convenções usadas por chaves no cache. O nome pode conter qualquer dado binário, mas recomendamos que você use cadeias de caracteres relativamente curtas e significativas para ajudar a garantir um bom desempenho e manutenção.

Observe também que o namespace usado pelos canais é separado do usado por chaves. Isso significa que você pode ter canais e chaves com o mesmo nome, embora isso possa tornar o código do aplicativo mais difícil de manter.

O segundo parâmetro é um Action delegado. O delegado é executado de forma assíncrona quando uma nova mensagem é exibida no canal. Neste exemplo, o delegado exibe a mensagem no console e a mensagem contém o título de uma postagem no blog.

Para publicar em um canal, um aplicativo pode usar o comando Redis PUBLISH. A biblioteca StackExchange fornece o IServer.PublishAsync método para executar essa operação. O próximo snippet de código mostra como publicar uma mensagem no canal "messages:blogPosts":

ConnectionMultiplexer redisHostConnection = ...;
ISubscriber subscriber = redisHostConnection.GetSubscriber();
...
BlogPost blogPost = ...;
subscriber.PublishAsync("messages:blogPosts", blogPost.Title);

Há vários pontos que você deve entender sobre o mecanismo de publicação/assinatura:

  • Vários assinantes podem assinar o mesmo canal e todos receberão as mensagens publicadas nesse canal.
  • Os assinantes recebem apenas mensagens que foram publicadas após a assinatura. Os canais não são armazenados em buffer e, depois que uma mensagem é publicada, a infraestrutura do Redis envia a mensagem por push para cada assinante e a remove.
  • Por padrão, as mensagens são recebidas pelos assinantes na ordem em que são enviadas. Em um sistema altamente ativo com um grande número de mensagens e muitos assinantes e editores, a entrega sequencial garantida de mensagens pode diminuir o desempenho do sistema. Se cada mensagem for independente e a ordem não for importante, você poderá habilitar o processamento simultâneo pelo sistema Redis, o que pode ajudar a melhorar a capacidade de resposta. Você pode fazer isso em um cliente StackExchange definindo PreserveAsyncOrder da conexão usada pelo assinante como false:
ConnectionMultiplexer redisHostConnection = ...;
redisHostConnection.PreserveAsyncOrder = false;
ISubscriber subscriber = redisHostConnection.GetSubscriber();

Considerações sobre serialização

Ao escolher um formato de serialização, considere as compensações entre desempenho, interoperabilidade, controle de versão, compatibilidade com sistemas existentes, compactação de dados e sobrecarga de memória. Ao avaliar o desempenho, lembre-se de que os parâmetros de comparação são altamente dependentes do contexto. Eles podem não refletir sua carga de trabalho real e podem não considerar bibliotecas ou versões mais recentes. Não há um único serializador "mais rápido" para todos os cenários.

Algumas opções a serem consideradas incluem:

  • Os Buffers de Protocolo (também chamados de protobuf) são um formato de serialização desenvolvido pelo Google para serializar dados estruturados com eficiência. Ele usa arquivos de definição fortemente tipados para definir estruturas de mensagens. Esses arquivos de definição são compilados para código específico do idioma para serializar e desserializar mensagens. O Protobuf pode ser usado em mecanismos RPC existentes ou pode gerar um serviço RPC.

  • O Apache Thrift usa uma abordagem semelhante, com arquivos de definição fortemente tipados e uma etapa de compilação para gerar o código de serialização e os serviços RPC.

  • O Apache Avro fornece funcionalidade semelhante a Protocol Buffers e Thrift, mas não há etapa de compilação. Em vez disso, os dados serializados sempre incluem um esquema que descreve a estrutura.

  • JSON é um padrão aberto que usa campos de texto legíveis por humanos. Ele tem amplo suporte multiplataforma. O JSON não usa esquemas de mensagens. Sendo um formato baseado em texto, ele não é muito eficiente na transmissão. Em alguns casos, no entanto, você pode estar retornando itens armazenados em cache diretamente para um cliente via HTTP, nesse caso, o armazenamento de JSON pode economizar o custo de desserialização de outro formato e, em seguida, serializar para JSON.

  • binário JSON (BSON) é um formato de serialização binária que usa uma estrutura semelhante ao JSON. O BSON foi projetado para ser leve, fácil de verificar e rápido para serializar e desserializar em relação ao JSON. Cargas são comparáveis em tamanho a JSON. Dependendo dos dados, uma carga BSON pode ser menor ou maior que uma carga JSON. O BSON tem alguns tipos de dados adicionais que não estão disponíveis no JSON, notadamente BinData (para matrizes de bytes) e Data.

  • MessagePack é um formato de serialização binária projetado para ser compacto para transmissão pelo fio. Não existem esquemas de mensagens ou verificação de tipo de mensagem.

  • O Bond é uma estrutura multiplataforma para trabalhar com dados esquematizados. Ele dá suporte à serialização e desserialização entre idiomas. Diferenças notáveis de outros sistemas listados aqui são suporte para herança, aliases de tipo e genéricos.

  • gRPC é um sistema RPC de software livre desenvolvido pelo Google. Por padrão, ele usa buffers de protocolo como sua linguagem de definição e formato de intercâmbio de mensagens subjacente.

Próximas etapas

Os seguintes padrões também podem ser relevantes para seu cenário ao implementar o cache em seus aplicativos:

  • Padrão de cache à parte: esse padrão descreve como carregar dados sob demanda em um cache a partir de um repositório de dados. Esse padrão também ajuda a manter a consistência entre os dados mantidos no cache e os dados no armazenamento de dados original.

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