Orientação de cache
A colocação em cache é uma técnica comum que visa melhorar o desempenho e a escalabilidade de um sistema. Ele armazena dados em cache copiando temporariamente os dados acessados com frequência para um armazenamento rápido localizado perto do aplicativo. Se este armazenamento de dados rápido estiver localizado mais próximo da aplicação que a origem original, a colocação em cache pode melhorar significativamente os tempos de resposta para aplicações cliente ao servir os dados mais rapidamente.
O cache é mais eficaz quando uma instância do cliente lê repetidamente os mesmos dados, especialmente se todas as seguintes condições se aplicarem ao armazenamento de dados original:
- Permanece relativamente estático.
- É lento em comparação com a velocidade da cache.
- Está sujeito a um elevado nível de contenção.
- Está longe quando a latência da rede pode fazer com que o acesso seja lento.
Armazenamento em cache em aplicativos distribuídos
Os aplicativos distribuídos normalmente implementam uma ou ambas as seguintes estratégias ao armazenar dados em cache:
- Eles usam um cache privado, onde 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 máquinas.
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 aplicativo de desktop. O cache do lado do servidor é feito pelo processo que fornece os serviços corporativos que estão sendo executados remotamente.
Cache privado
O tipo mais básico de cache é um armazenamento 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 é de acesso rápido. Também pode fornecer um meio eficaz para armazenar quantidades modestas de dados estáticos. O tamanho de um cache normalmente é limitado pela quantidade de memória disponível na máquina 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 será mais lento para acessar 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 simultânea, cada instância de aplicativo terá seu próprio cache independente contendo sua própria cópia dos dados.
Pense em um cache como um instantâneo dos dados originais em algum momento no passado. Se esses dados não forem estáticos, é provável que instâncias de aplicativos diferentes mantenham versões diferentes dos dados em seus caches. Portanto, a mesma consulta realizada por essas instâncias pode retornar resultados diferentes, como mostra a Figura 1.
Figura 1: Usando um cache na memória em diferentes instâncias de um aplicativo.
Cache compartilhado
Se você usar um cache compartilhado, isso pode ajudar a aliviar as preocupações de que os dados possam diferir em cada cache, o que pode ocorrer com o cache na memória. O cache compartilhado garante que diferentes instâncias de aplicativos vejam a mesma exibição dos dados armazenados em cache. Ele localiza o cache em um local separado, que normalmente é hospedado como parte de um serviço separado, como mostra a Figura 2.
Figura 2: Usando um cache compartilhado.
Um benefício importante da abordagem de cache compartilhado é a escalabilidade que ela oferece. 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 simplesmente envia uma solicitação para o serviço de cache. A infraestrutura subjacente determina o local dos dados armazenados em cache no cluster. Você pode facilmente dimensionar o cache adicionando mais servidores.
Há duas desvantagens principais da abordagem de cache compartilhado:
- O acesso ao cache é mais lento porque não é mais mantido localmente em cada instância do aplicativo.
- O requisito de implementar um serviço de cache separado pode adicionar complexidade à solução.
Considerações sobre o uso de cache
As seções a seguir descrevem com mais detalhes as 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 quanto 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 suportar um número limitado de conexões simultâneas. No entanto, recuperar dados de um cache compartilhado, 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 que são 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 armazenamento autorizado de informações críticas. Em vez disso, certifique-se de que todas as alterações que seu aplicativo não pode perder sejam sempre salvas em um armazenamento de dados persistente. Se o cache não estiver disponível, seu aplicativo ainda poderá continuar a operar usando o armazenamento de dados e você não perderá informações importantes.
Determinar como armazenar dados em cache de forma eficaz
A chave para usar um cache efetivamente está em determinar os 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 que são recuperados por um aplicativo. O aplicativo precisa buscar os dados apenas uma vez no armazenamento de dados e esse acesso subsequente pode ser satisfeito 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 total ou parcialmente um cache e escolher os dados a serem armazenados em cache. Por exemplo, você pode semear o cache com os dados de perfil de usuário estáticos 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 imutáveis ou que mudam com pouca frequência. Os exemplos incluem informações de referência, como informações sobre 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 de recursos e melhorar o desempenho. Você também pode querer ter um processo em segundo plano que atualize periodicamente os dados de referência no cache para garantir que ele esteja up-to-date. 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 Armazenar dados altamente dinâmicos em cache mais adiante neste artigo para obter mais informações). Quando os dados originais mudam regularmente, as informações armazenadas em cache tornam-se 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 de valores múltiplos, como um cliente bancário com nome, endereço e saldo de conta, alguns desses elementos podem 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.
Recomendamos que você realize testes de desempenho e análise de uso para determinar se o preenchimento prévio ou o carregamento sob demanda do cache, ou uma combinação de ambos, é apropriado. A decisão deve basear-se na volatilidade e no padrão de utilização dos dados. A utilização do cache e a análise de desempenho são importantes em aplicativos que enfrentam cargas pesadas e devem ser altamente escaláveis. Por exemplo, em cenários altamente escaláveis, você pode semear o cache para reduzir a carga no armazenamento de dados nos 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 transforma dados ou executa um cálculo complicado, ela pode salvar os resultados da operação no cache. Se o mesmo cálculo for necessário depois, o aplicativo pode simplesmente recuperar os resultados do cache.
Um aplicativo pode modificar dados mantidos em 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 apenas 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.
Armazene dados altamente dinâmicos em cache
Quando você armazena informações que mudam rapidamente em um armazenamento de dados persistente, isso pode impor uma sobrecarga ao sistema. Por exemplo, considere um dispositivo que relata continuamente o status ou alguma outra medição. Se um aplicativo optar por não armazenar esses dados em cache com base no fato de que as informações armazenadas em cache quase sempre estarão desatualizadas, a mesma consideração pode ser verdadeira ao armazenar e recuperar essas informações do armazenamento de dados. No tempo necessário para salvar e buscar esses dados, eles podem ter mudado.
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 importa se a alteração ocasional é perdida.
Gerenciar a expiração de dados em um cache
Na maioria dos casos, os dados mantidos em cache são uma cópia dos dados mantidos no armazenamento de dados original. Os dados no armazenamento de dados original podem ser alterados depois de serem armazenados em cache, fazendo com que os dados armazenados em cache se tornem obsoletos. Muitos sistemas de cache permitem configurar o cache para expirar dados e reduzir o período durante 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 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.
Observação
Considere cuidadosamente o período de expiração do cache e dos objetos que ele contém. Se você torná-lo muito curto, os objetos expirarão muito rapidamente e você reduzirá os benefícios de usar o cache. Se tornar o período demasiado longo, corre o risco de os dados se tornarem obsoletos.
Também é possível que o cache seja preenchido se os dados puderem permanecer residentes por um longo tempo. Nesse caso, quaisquer solicitações para adicionar novos itens ao cache podem 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, corre 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. Existem vários tipos de políticas de despejo. Estes são, entre outros:
- Uma política usada mais recentemente (na expectativa de que os dados não sejam necessários novamente).
- Uma política de primeiro a entrar, primeiro a sair (os dados mais antigos são removidos primeiro).
- Uma política de remoção explícita com base em um evento acionado (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 ao 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 serve dados por meio de uma conexão HTTP, poderá forçar implicitamente um cliente da Web (como um navegador ou proxy da Web) a buscar as informações mais recentes. Você pode fazer isso se um recurso for atualizado por uma alteração no URI desse recurso. Os clientes da 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 da Web ignorará todas as versões previamente armazenadas em cache 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:
- Otimista. Imediatamente antes de atualizar os dados, o aplicativo verifica se os dados no cache foram alterados desde que foram recuperados. Se os dados ainda forem os mesmos, a alteração pode ser feita. Caso contrário, o aplicativo tem que decidir se deseja atualizá-lo. (A lógica de negócios que orienta essa decisão será específica do aplicativo.) Essa abordagem é adequada para situações em que as atualizações são pouco frequentes ou onde é improvável que ocorram colisões.
- Pessimista. Quando 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 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 as colisões são mais prováveis, especialmente se um aplicativo atualizar vários itens no cache e deve garantir que essas alterações sejam aplicadas de forma consistente.
Implemente alta disponibilidade e escalabilidade e melhore o desempenho
Evite usar um cache como repositório primário de dados; Essa é a função do armazenamento de dados original a partir 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 na 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 parar de responder ou falhar enquanto aguarda a retomada do serviço de cache.
Portanto, o aplicativo deve estar preparado para detetar a disponibilidade do serviço de cache e voltar ao armazenamento de dados original se o cache estiver inacessível. O padrãoCircuit-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 preenchido novamente à medida que os dados são lidos do armazenamento de dados original, seguindo uma estratégia como o padrão Cache-side.
No entanto, a escalabilidade do sistema pode ser afetada se o aplicativo retornar 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 e 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, finalmente, 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 evitar que o cache local se torne muito obsoleto em relação ao cache compartilhado. No entanto, o cache local atua como um buffer se o cache compartilhado estiver inacessível. A Figura 3 mostra essa estrutura.
Figura 3: Usando um cache privado local com um cache compartilhado.
Para oferecer suporte a caches grandes que armazenam dados de vida relativamente longa, alguns serviços de cache fornecem uma opção de alta disponibilidade que implementa failover automático se o cache ficar indisponível. Essa abordagem geralmente envolve a replicação dos dados armazenados em cache 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 do 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 suportam a capacidade de adicionar (e remover) nós dinamicamente e reequilibrar os dados entre partições. Essa abordagem pode envolver clustering, no qual a coleção de nós é apresentada aos 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 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 a replicação e o 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 ou objetos de dados únicos. 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 de grande escala fornecem operações em lote para esses fins. 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 pequenas solicitações.
Cache e eventual consistência
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 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 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.
Proteja os dados armazenados em cache
Independentemente do serviço de cache usado, considere como proteger os dados mantidos no cache contra acesso não autorizado. Existem duas preocupações principais:
- A privacidade dos dados no cache.
- A privacidade dos dados à medida que 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 exija que os aplicativos especifiquem o seguinte:
- Quais identidades podem acessar dados no cache.
- Quais operações (leitura e gravação) essas identidades podem realizar.
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 quaisquer dados no cache.
Se precisar restringir o acesso a subconjuntos dos dados armazenados em cache, siga um destes procedimentos:
- Divida o cache em partições (usando servidores de cache diferentes) e conceda acesso apenas às identidades para as partições que eles devem ter permissão para usar.
- Criptografe os dados em cada subconjunto usando chaves diferentes e forneça as chaves de criptografia somente para identidades que devem ter acesso a cada subconjunto. Um aplicativo cliente ainda poderá recuperar todos os dados no cache, mas só poderá descriptografar os dados para os quais possui as chaves.
Você também deve proteger os dados à medida que eles entram e saem 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 pode não exigir 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 SSL.
Considerações para implementar o cache no Azure
O Cache Redis do Azure é uma implementação do cache Redis de código aberto 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 tenham a chave de acesso apropriada.
O Cache Redis do Azure é uma solução de cache de alto desempenho que fornece disponibilidade, escalabilidade e segurança. Normalmente, é executado como um serviço distribuído por uma ou mais máquinas dedicadas. 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 Redis do Azure é compatível com muitas das várias APIs usadas por aplicativos cliente. Se você tiver aplicativos existentes que já usam o Cache Redis do Azure em execução local, o Cache do Azure para Redis fornecerá um caminho de migração rápida para o cache na nuvem.
Características do Redis
O Redis é mais do que um simples servidor de cache. Ele fornece um banco de dados distribuído na memória com um extenso conjunto de comandos que suporta muitos cenários comuns. Eles são descritos mais adiante neste documento, na seção Usando cache Redis. Esta seção resume alguns dos principais recursos que o Redis oferece.
Redis como um banco de dados na memória
O Redis suporta operações de leitura e gravação. No Redis, as gravações podem ser protegidas contra falhas do sistema sendo 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 que os clientes leiam e gravem 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 Persistência do Redis no site do Redis.
Observação
O Redis não garante que todas as gravações serão salvas se houver uma falha catastrófica, mas, na pior das hipóteses, você pode perder apenas alguns segundos de dados. Lembre-se de que um cache não se destina a atuar como uma fonte de dados autorizada 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-side.
Tipos de dados Redis
O Redis é um armazenamento de chave-valor, onde os valores podem conter tipos simples ou estruturas de dados complexas, como hashes, listas e conjuntos. Ele suporta um conjunto de operações atômicas nesses tipos de dados. As chaves podem ser permanentes ou marcadas com um tempo de vida 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 oferece 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 do 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 servir 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 Redis.
O Redis também fornece clustering, que permite particionar dados de forma transparente em fragmentos entre servidores e distribuir a carga. Esse recurso melhora a escalabilidade, porque 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 replicação primária/subordinada. Isso garante a disponibilidade em cada nó do 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 da 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 é automaticamente removida 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 se enche, o Redis pode remover automaticamente chaves e seus valores seguindo várias políticas. O padrão é LRU (usado menos recentemente), mas você também pode selecionar outras políticas, como remover chaves aleatoriamente ou desativar completamente a remoção (caso em que as tentativas de adicionar itens ao cache falham se ele estiver cheio). A página Usando Redis como um cache LRU fornece mais informações.
Transações e lotes 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 são garantidos para serem executados sequencialmente, e nenhum comando emitido por outros clientes simultâneos será entrelaçado entre eles.
No entanto, essas não são transações verdadeiras como 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 comandos, 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 recusa a processar toda a transação e a descarta.
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 nenhum comando que já tenha sido executado. Esta 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 suporta 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 lote é processado, cada comando é executado. Se algum desses comandos estiver malformado, eles serão rejeitados (o que não acontece com uma transação), mas os comandos restantes serão executados. Também não há garantia sobre a ordem em que os comandos no lote serão processados.
Segurança Redis
O Redis é focado exclusivamente em fornecer acesso rápido aos dados e foi projetado para ser executado dentro de um ambiente confiável que pode ser acessado apenas por clientes confiáveis. O Redis suporta um modelo de segurança limitado baseado na autenticação de senha. (É possível remover completamente a autenticação, embora não recomendemos 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 do 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 aos comandos desabilitando-os ou renomeando-os (e fornecendo apenas clientes privilegiados com os novos nomes).
O Redis não suporta diretamente nenhuma forma de criptografia de dados, portanto, toda a 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 a implementação de um proxy SSL.
Para obter mais informações, visite a página de segurança do Redis no site do Redis.
Observação
O Cache Redis do Azure fornece sua própria camada de segurança por meio da qual os clientes se conectam. Os servidores Redis subjacentes não estão expostos à rede pública.
Cache Redis do Azure
O Cache Redis do Azure fornece acesso aos servidores Redis hospedados em um datacenter do Azure. Funciona como uma fachada que proporciona controlo de acessos e segurança. Você pode provisionar um cache usando o portal do Azure.
O portal fornece uma série de configurações predefinidas. Eles variam de um cache de 53 GB executado como um serviço dedicado que suporta comunicações SSL (para privacidade) e replicação mestre/subordinada com um contrato de nível de serviço (SLA) de 99,9% de disponibilidade, até um cache de 250 MB sem replicação (sem garantias de disponibilidade) em execução em 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 de 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 através 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 à força no disco.
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 que estão sendo executadas, o volume de leituras e gravações e o número de acertos de cache versus falhas 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 falhas de cache exceder um valor especificado na última hora, porque isso significa que o cache pode ser muito pequeno ou os dados podem estar sendo removidos muito rapidamente.
Você também pode monitorar o uso da CPU, da memória e da rede para o cache.
Para obter mais informações e exemplos mostrando 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 que são executados usando funções Web do Azure, poderá salvar informações de estado da sessão e saída HTML em um Cache do Azure para Redis. O provedor de estado de sessão para o Cache Redis do Azure permite que você compartilhe informações de sessão entre diferentes instâncias de um aplicativo Web ASP.NET e é muito útil em situações de web farm em que a afinidade cliente-servidor não está disponível e o armazenamento em cache de dados de sessão na memória não seria apropriado.
Usar o provedor de estado de sessão com o Cache Redis do Azure 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 a acesso controlado e simultâneo aos mesmos dados de estado da sessão para vários leitores e um único gravador.
- Usando a compactação para economizar 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.
Observação
Não use o provedor de estado de sessão para o Cache Redis do Azure com aplicativos ASP.NET 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 armazenamento em cache de dados.
Da mesma forma, o provedor de cache de saída para o Cache Redis do Azure 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 Redis do Azure pode melhorar os tempos de resposta de aplicativos que processam saída HTML complexa. As 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 novamente. 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 Redis do Azure 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), poderá criar e hospedar seus próprios servidores Redis usando as 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ê deseja criar um cluster, precisará de vários servidores primários e subordinados. Uma topologia de replicação clusterizada 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 ser executado em diferentes datacenters do Azure localizados em regiões diferentes, se você desejar localizar dados armazenados em cache perto dos aplicativos com maior probabilidade de usá-los. Para obter um exemplo de criação e configuração de um nó Redis em execução como uma VM do Azure, consulte Executando o Redis em uma VM Linux do CentOS no Azure.
Observação
Se você implementar seu próprio cache Redis dessa maneira, será responsável por monitorar, gerenciar e proteger o serviço.
Particionando um cache Redis
O particionamento do cache envolve a divisão do cache em vários computadores. Essa estrutura oferece várias vantagens em relação ao uso de um único servidor de cache, incluindo:
- Criação de um cache muito maior do que pode ser armazenado em um único servidor.
- Distribuição de dados entre servidores, melhorando a disponibilidade. Se um servidor falhar ou ficar inacessível, os dados que ele manté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. Os dados armazenados em cache em um servidor que se torna inacessível podem ser armazenados em cache em um servidor diferente.
- Distribuir a carga pelos servidores, melhorando assim o desempenho e a escalabilidade.
- Geolocalização de dados perto dos 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 uma variedade de abordagens para distribuir os dados. O padrão Sharding fornece mais informações sobre a implementação de fragmentação.
Para implementar o particionamento em um cache Redis, você pode adotar uma das seguintes abordagens:
- Roteamento de consultas 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 manté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, executará a operação solicitada. Caso contrário, ele encaminhará a solicitação para o servidor apropriado. Este modelo é implementado pelo cluster 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. Neste 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 Cache Redis do Azure (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 forem criados Cache Redis do Azure adicionais, por exemplo), os aplicativos cliente talvez precisem 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 de 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 usar o particionamento do lado do cliente.
A página Particionamento: 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 de cache Redis
O Redis suporta aplicações cliente escritas em várias linguagens de programação. Se você criar novos aplicativos usando o .NET Framework, recomendamos que você use a biblioteca de cliente StackExchange.Redis. Esta 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 Connect
método estático ConnectionMultiplexer
da classe. A conexão que esse método cria é projetada para ser usada durante toda a vida útil do aplicativo cliente, e a mesma conexão pode ser usada por vários threads simultâneos. Não reconecte e desconecte cada vez que 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 Redis do Azure 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 StringGet
métodos and StringSet
. Esses métodos esperam uma chave como 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 da localização do servidor Redis, muitas operações podem incorrer em alguma latência enquanto uma solicitação é transmitida ao servidor e uma resposta é retornada ao cliente. A biblioteca StackExchange fornece versões assíncronas de muitos dos métodos que expõe para ajudar os aplicativos cliente a permanecerem responsivos. Esses métodos suportam o padrão assíncrono baseado em tarefas no .NET Framework.
O trecho 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 do StringGet
).
Se o item não for encontrado, ele será buscado 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 possa ser recuperado mais rapidamente da 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
não se restringem a recuperar ou armazenar valores de cadeia de caracteres. Eles podem tomar qualquer item que é 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 StringGet
método e desserializando-o 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 alguns códigos de exemplo que usam 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-side:
// 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 suporta pipelining de comandos 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.
Esta abordagem ajuda a reduzir a latência, fazendo um uso mais eficiente da rede. O trecho 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 esperar 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 informações adicionais 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 multiplexadores 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 questões de cache são pares chave-valor onde 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.
Observe que as chaves também contêm dados não interpretados, portanto, você pode usar qualquer informação binária como a chave. No entanto, quanto mais longa for a chave, mais espaço será necessário para armazenar e mais tempo levará para executar operações de pesquisa. Para usabilidade e facilidade de manutenção, projete seu espaço de chaves cuidadosamente e use chaves significativas (mas não detalhadas).
Por exemplo, use chaves estruturadas como "customer:100" para representar a chave do cliente com ID 100 em vez de simplesmente "100". Esse esquema permite distinguir facilmente entre valores que armazenam diferentes tipos de dados. Por exemplo, você também pode usar a chave "orders:100" para representar a chave para o pedido com ID 100.
Além das cadeias 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 cliente 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 lote
O Redis suporta uma série de operações atômicas get-and-set em valores de cadeia de caracteres. Essas operações removem os possíveis perigos de corrida que podem ocorrer ao usar comandos e GET
separadosSET
. As operações disponíveis incluem:
INCR
,INCRBY
,DECR
, eDECRBY
, que executam operações de incremento atômico e decréscimo em valores de dados numéricos inteiros. A biblioteca StackExchange fornece versões sobrecarregadas dosIDatabase.StringIncrementAsync
métodos eIDatabase.StringDecrementAsync
para executar essas operações e retornar o valor resultante armazenado no cache. O trecho 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 o altera para um novo valor. A biblioteca StackExchange disponibiliza essa operação por meio doIDatabase.StringGetSetAsync
método. O trecho de código abaixo 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 a zero, tudo como parte da mesma operação:ConnectionMultiplexer redisHostConnection = ...; IDatabase cache = redisHostConnection.GetDatabase(); ... string oldValue = await cache.StringGetSetAsync("data:counter", 0);
MGET
eMSET
, que pode retornar ou alterar um conjunto de valores de cadeia de caracteres como uma única operação. OsIDatabase.StringGetAsync
métodos eIDatabase.StringSetAsync
estão sobrecarregados para oferecer 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 através 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 que é semelhante 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 trecho de código a seguir mostra um exemplo que incrementa e diminui 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 das transações em bancos de dados relacionais. O Execute
método simplesmente enfileira todos os comandos que compõem a transação a ser executada e, se algum deles estiver malformado, 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ê deve buscar os resultados do comando usando a propriedade Result da tarefa correspondente, conforme mostrado no exemplo acima. A leitura da propriedade Result 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. Esta 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.
Você cria um IBatch
objeto usando o IDatabase.CreateBatch
método e, em seguida, executa o lote usando o IBatch.Execute
método, conforme mostrado no exemplo a seguir. Esse código simplesmente define um valor de cadeia de caracteres, incrementa e diminui 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 está malformado, os outros comandos ainda podem ser executados. O IBatch.Execute
método não retorna nenhuma indicação de sucesso ou fracasso.
Execute operações de disparo e esqueça o cache
O Redis suporta operações de fogo e esquecimento usando sinalizadores de comando. Nessa situação, o cliente simplesmente inicia uma operação, mas não tem interesse no resultado e não espera que o comando seja concluído. O exemplo abaixo mostra como executar o comando INCR como uma operação de incêndio e esquecimento:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
await cache.StringSetAsync("data:key1", 99);
...
cache.StringIncrement("data:key1", flags: CommandFlags.FireAndForget);
Especificar chaves que expiram automaticamente
Ao armazenar um item em um cache Redis, você pode especificar um tempo limite após o qual o item será removido automaticamente do cache. Você também pode consultar quanto tempo mais uma chave tem antes de expirar usando o TTL
comando. Este comando está disponível para aplicativos StackExchange usando o IDatabase.KeyTimeToLive
método.
O trecho 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 o tempo de expiração para uma data e hora específicas usando o comando EXPIRE , que está disponível na biblioteca do 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));
...
Sugestão
Você pode remover manualmente um item do cache usando o comando DEL, que está disponível através da biblioteca StackExchange como o IDatabase.KeyDeleteAsync
método.
Usar tags para correlacionar 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 (set difference), SINTER (set intersection) e SUNION (set union). A biblioteca StackExchange unifica essas operações no IDatabase.SetCombineAsync
método. O primeiro parâmetro para este método especifica a operação definida a ser executada.
Os trechos 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 de cache Redis anteriormente neste artigo.
Um BlogPost
objeto contém quatro campos: um ID, um título, uma pontuação de classificação e uma coleção de tags. O primeiro trecho de código abaixo mostra os dados de exemplo usados para preencher uma lista de objetos em BlogPost
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 tags de cada BlogPost
objeto como um conjunto em um cache Redis e associar cada conjunto à ID do BlogPost
. Isso permite que um aplicativo encontre rapidamente todas as tags 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 tag específica, você pode criar outro conjunto que contenha as postagens de blog fazendo referência ao ID da tag 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 de forma muito eficiente. Por exemplo, você pode encontrar e exibir todas as tags da postagem de blog 1 da seguinte 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 tags que são 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 tag 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);
}
Encontrar itens acessados recentemente
Uma tarefa comum exigida de muitos aplicativos é encontrar 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 Redis. Uma lista Redis contém vários itens que compartilham a mesma chave. A lista funciona como uma fila dupla. 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 trechos de código abaixo mostram como você pode executar essas operações usando a biblioteca StackExchange. Este código usa o BlogPost
tipo dos exemplos anteriores. Como uma postagem de blog é lida por um usuário, o IDatabase.ListLeftPushAsync
método envia o título da postagem de 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 posts são lidos, seus títulos são empurrados para a mesma lista. A lista é ordenada pela sequência em que os títulos foram adicionados. Os posts de blog lidos mais recentemente estão na extremidade esquerda 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 inicial e um ponto final. O código a seguir recupera os títulos das 10 postagens de blog (itens de 0 a 9) na extremidade mais à esquerda da lista:
// Show latest ten posts
foreach (string postTitle in await cache.ListRangeAsync(redisKey, 0, 9))
{
Console.WriteLine(postTitle);
}
Observe que o ListRangeAsync
método não remove itens da lista. Para fazer isso, você pode usar os IDatabase.ListLeftPopAsync
métodos e IDatabase.ListRightPopAsync
.
Para evitar que a lista cresça indefinidamente, você pode eliminar itens periodicamente cortando a lista. O trecho de código abaixo mostra como remover todos, exceto os cinco itens mais à esquerda da lista:
await cache.ListTrimAsync(redisKey, 0, 5);
Implementar uma tabela 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 trecho 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 de 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 e pontuações das postagens do blog em ordem crescente de pontuação usando o IDatabase.SortedSetRangeByRankWithScores
método:
foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(redisKey))
{
Console.WriteLine(post);
}
Observação
A biblioteca StackExchange também fornece o IDatabase.SortedSetRangeByRankAsync
método, que retorna os dados em 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 que são retornados fornecendo parâmetros adicionais para o IDatabase.SortedSetRangeByRankWithScoresAsync
método. O próximo exemplo exibe os títulos e pontuações das 10 melhores 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 àqueles 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 através de um mecanismo de editor/assinante de alto desempenho. Os aplicativos cliente podem se inscrever em um canal e outros aplicativos ou serviços podem publicar mensagens no canal. Os aplicativos de assinatura receberão essas mensagens e poderão processá-las.
O Redis fornece o comando SUBSCRIBE para os aplicativos cliente usarem para se inscrever em canais. Este comando espera o nome de um ou mais canais nos quais o aplicativo aceitará mensagens. A biblioteca StackExchange inclui a ISubscription
interface, que permite que um aplicativo .NET Framework se inscreva 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 se inscrever em 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 pelas chaves no cache. O nome pode conter quaisquer dados binários, mas recomendamos que você use cadeias de caracteres relativamente curtas e significativas para ajudar a garantir um bom desempenho e capacidade de manutenção.
Observe também que o namespace usado pelos canais é separado daquele usado pelas 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 delegado de ação. Este delegado é executado de forma assíncrona sempre que uma nova mensagem aparece no canal. Este exemplo simplesmente exibe a mensagem no console (a mensagem conterá o título de uma postagem de blog).
Para publicar em um canal, um aplicativo pode usar o comando Redis PUBLISHING. A biblioteca StackExchange fornece o IServer.PublishAsync
método para executar essa operação. O próximo trecho 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);
Existem vários pontos que você deve entender sobre o mecanismo de publicação/assinatura:
- Vários subscritores podem inscrever-se no mesmo canal e todos receberão as mensagens publicadas nesse canal.
- Os subscritores só recebem mensagens que tenham sido publicadas depois de se terem inscrito. Os canais não são armazenados em buffer e, quando uma mensagem é publicada, a infraestrutura Redis envia a mensagem para cada assinante e, em seguida, 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 retardar o desempenho do sistema. Se cada mensagem for independente e a ordem não for importante, você pode habilitar o processamento simultâneo pelo sistema Redis, o que pode ajudar a melhorar a capacidade de resposta. Você pode conseguir isso em um cliente StackExchange definindo o 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 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 benchmarks 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 considerar incluem:
Protocol Buffers (também chamado protobuf) é um formato de serialização desenvolvido pelo Google para serializar dados estruturados de forma eficiente. Ele usa arquivos de definição fortemente tipados para definir estruturas de mensagem. Esses arquivos de definição são então compilados para código específico da linguagem para serializar e desserializar mensagens. Protobuf pode ser usado sobre 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 serviços RPC.
O Apache Avro fornece funcionalidade semelhante ao Protocol Buffers e Thrift, mas não há nenhuma 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. Tem amplo suporte multiplataforma. JSON não usa esquemas de mensagem. Sendo um formato baseado em texto, não é muito eficiente através do fio. Em alguns casos, no entanto, você pode estar retornando itens armazenados em cache diretamente para um cliente via HTTP, caso em que armazenar JSON pode economizar o custo de desserialização de outro formato e, em seguida, serializar para JSON.
binary JSON (BSON) é um formato de serialização binário que usa uma estrutura semelhante ao JSON. O BSON foi projetado para ser leve, fácil de digitalizar e rápido de serializar e desserializar, em relação ao JSON. As cargas úteis são comparáveis em tamanho ao JSON. Dependendo dos dados, uma carga útil BSON pode ser menor ou maior do que uma carga JSON útil. O BSON tem alguns tipos de dados adicionais que não estão disponíveis em JSON, notavelmente BinData (para matrizes de bytes) e Date.
MessagePack é um formato de serialização binário projetado para ser compacto para transmissão por fio. Não há esquemas de mensagem ou verificação de tipo de mensagem.
O Bond é uma estrutura multiplataforma para trabalhar com dados esquematizados. Ele suporta serialização e desserialização entre idiomas. Diferenças notáveis em relação a outros sistemas listados aqui são o suporte para herança, aliases de tipo e genéricos.
gRPC é um sistema RPC de código aberto 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óximos passos
- documentação do Cache do Azure para Redis
- Perguntas frequentes sobre o Cache do Azure para Redis
- Padrão assíncrono baseado em tarefas
- Documentação Redis
- StackExchange.Redis
- Guia de particionamento de dados
Recursos relacionados
Os padrões a seguir também podem ser relevantes para seu cenário quando você implementa o cache em seus aplicativos:
Padrão de cache-side: esse padrão descreve como carregar dados sob demanda em um cache a partir de um armazenamento 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 Sharding fornece informações sobre a implementação de particionamento horizontal para ajudar a melhorar a escalabilidade ao armazenar e acessar grandes volumes de dados.