Documentação de orientação sobre a colocação em cache

Azure Cache for Redis

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.

A colocação em cache é mais eficaz quando uma instância de cliente lê repetidamente os mesmos dados, especialmente se todas as condições seguintes se aplicarem ao arquivo 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 de rede pode tornar o acesso lento.

Colocação em cache em aplicações distribuídas

Normalmente, as aplicações distribuídas implementam uma ou ambas as estratégias seguintes ao colocar 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. A colocação em cache do lado do cliente é realizada pelo processo que fornece a interface de utilizador para um sistema, como um browser ou uma aplicação de ambiente de trabalho. A colocação em cache do lado do servidor é realizada pelo processo que fornece os serviços empresariais que estão a ser executados remotamente.

Colocação em cache privada

O tipo mais básico de cache é um arquivo dentro da memória. É mantido no espaço de endereços de um processo único e acedido 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 precisar de colocar em cache mais informações do que é fisicamente possível na memória, pode escrever dados em cache no sistema de ficheiros 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 tiver várias instâncias de uma aplicação que utiliza este modelo em execução em simultâneo, cada instância da aplicação tem a sua própria cache independente que contém a sua própria cópia dos dados.

Pense numa 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. Por conseguinte, a mesma consulta realizada por estas instâncias pode devolver resultados diferentes, conforme mostra a Figura 1.

The results of using an in-memory cache in different instances of an application

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

Colocação em cache partilhada

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. A colocação em cache partilhada garante que diferentes instâncias da aplicação veem a mesma vista de dados 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.

The results of using a shared cache

Figura 2: Usando um cache compartilhado.

Uma vantagem importante da abordagem de colocação em cache partilhada é a escalabilidade que 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 aplicação simplesmente envia um pedido para o serviço de cache. A infraestrutura subjacente determina o local dos dados armazenados em cache no cluster. Pode dimensionar facilmente a cache ao adicionar mais servidores.

Existem duas desvantagens principais da abordagem de colocação em cache partilhada:

  • O cache é mais lento para acessar porque não é mais mantido localmente em cada instância do aplicativo.
  • O requisito de implementação de um serviço de cache separado poderá adicionar complexidade à solução.

Considerações sobre a utilização da colocação em cache

As secções seguintes descrevem mais pormenorizadamente as considerações sobre a conceção e utilização de uma cache.

Decidir quando colocar dados em cache

A colocação em cache pode melhorar significativamente o desempenho, a escalabilidade e a disponibilidade. Quanto mais dados tiver e quanto maior for o número de utilizadores que precisam de aceder a estes dados, maiores serão as vantagens da colocação em 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, uma base de dados poderá suportar um número limitado de ligações simultâneas. No entanto, a obtenção de dados de uma cache partilhada, em vez da base de dados subjacente, permite que uma aplicação cliente aceda a estes dados, mesmo se o número de ligações disponíveis estiver esgotado no momento. Além disso, se a base de dados ficar indisponível, as aplicações cliente poderão continuar ao utilizarem os dados que são mantidos na cache.

Considere a colocação em cache de dados que são lidos com frequência, mas modificados com pouca frequência (por exemplo, os dados que têm uma maior proporção de operações de leitura do que operações de escrita). No entanto, não recomendamos a utilização da cache como o arquivo autoritativo de informações importantes. 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 colocar dados em cache de forma eficaz

A solução para utilizar uma cache de forma eficaz passa por determinar os dados mais adequados para colocar em cache e colocá-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.

Em alternativa, uma cache pode ser total ou parcialmente preenchida com dados antecipadamente, normalmente quando a aplicação é iniciada (uma abordagem conhecida como seeding). No entanto, poderá não ser aconselhável implementar o seeding para uma cache de grandes dimensões porque esta abordagem pode impor uma carga elevada repentina no arquivo de dados original quando a aplicação entrar em execução.

Muitas vezes, uma análise de padrões de utilização pode ajudar a decidir se deve pré-povoar total ou parcialmente uma cache e a escolher os dados a colocar 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.

A colocação em cache normalmente funciona bem com os dados que são imutáveis ou que são alterados com pouca frequência. Alguns exemplos incluem informações de referência, tais como informações sobre preços e produtos numa aplicação de comércio eletrónico, ou recursos estáticos partilhados cuja construção é dispendiosa. A totalidade ou parte destes dados podem ser carregados para a cache durante o arranque da aplicação para minimizar a exigência sobre os recursos e para 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 eles estejam atualizados. Ou, o processo em segundo plano pode atualizar o cache quando os dados de referência são alterados.

A colocação em cache é menos útil para dados dinâmicos, embora existam algumas exceções a esta consideração (veja a secção «Colocar em cache dados altamente dinâmicos» mais à frente 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 no padrão de utilização e volatilidade 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.

A colocação em cache também pode ser utilizada para evitar a repetição de cálculos enquanto a aplicação está em execução. Se uma operação transformar dados ou realizar um cálculo complicado, pode guardar os resultados da operação na cache. Se o mesmo cálculo for necessário mais tarde, a aplicação pode simplesmente obter os resultados da cache.

Uma aplicação pode modificar os dados que são mantidos numa cache. No entanto, recomendamos pensar na cache como um arquivo de dados transitório que poderá desaparecer em qualquer altura. Não armazene dados valiosos apenas no cache; Certifique-se de manter as informações no armazenamento de dados original também. Isto significa que se a cache ficar indisponível, minimiza a possibilidade de perda de dados.

Colocar em cache dados altamente dinâmicos

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 reporta continuamente um estado ou qualquer outra medida. Se uma aplicação optar por não colocar em cache estes dados com base no facto de que as informações em cache estarão quase sempre desatualizadas, a mesma consideração poderá ser verdadeira ao armazenar e obter estas informações a partir do arquivo de dados. No tempo que demora a guardar e a obter estes dados, poderão já ter sido alterados.

Numa situação como esta, considere as vantagens de armazenar as informações dinâmicas diretamente na cache em vez de armazenar no arquivo 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.

Gerir a expiração de dados numa cache

Na maioria dos casos, os dados que são mantidos numa cache são uma cópia dos dados que estão contidos no arquivo de dados original. Os dados no arquivo de dados original poderão sofrer alterações depois de serem colocados em cache, o que faz com que os dados em cache se tornem obsoletos. Muitos sistemas de colocação em cache permitem configurar a 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). Pode definir uma política de expiração predefinida quando configurar a cache. Em muitos serviços de cache, também pode estipular o período de expiração para objetos individuais quando os armazena através de programação na 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. Esta definição substitui qualquer política de expiração em toda a cache, mas apenas para os objetos especificados.

Nota

Considere o período de expiração para a cache e os objetos que contém cuidadosamente. Se definir um período demasiado curto, os objetos irão expirar demasiado rápido e reduzirá as vantagens de utilizar a cache. Se definir um período demasiado longo, corre o risco de os dados se tornarem obsoletos.

Também é possível que a cache fique cheia se permitir que os dados permaneçam residentes durante muito tempo. Neste caso, quaisquer pedidos para adicionar novos itens à cache poderão causar a remoção forçada de alguns itens num processo conhecido como expulsão. Normalmente, os serviços de cache expulsam dados numa base menos recentemente utilizados (LRU), mas, normalmente, pode substituir esta política e impedir que os itens sejam expulsos. No entanto, se adotar esta abordagem, corre o risco de exceder a memória disponível na cache. Uma aplicação que tente adicionar um item à cache irá falhar com uma exceção.

Algumas implementações de colocação em cache poderão fornecer políticas adicionais de expulsão. Existem vários tipos de políticas de expulsão. 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 first-in, first-out (os dados mais antigos são expulsos em primeiro lugar).
  • Uma política de remoção explícita com base num evento acionado (por exemplo, os dados estão a ser modificados).

Invalidar dados numa cache do lado do cliente

Normalmente, considera-se que os dados que são mantidos numa cache do lado do cliente estão 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.

Isto significa que é possível que um cliente que utiliza uma cache mal configurada continue a utilizar as informações desatualizadas. Por exemplo, se as políticas de expiração da cache não forem corretamente implementadas, um cliente poderá utilizar informações desatualizadas que são colocadas em cache localmente quando as informações na origem 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. Pode fazê-lo se um recurso for atualizado por uma alteração no URI desse recurso. Normalmente,os clientes Web utilizam o URI de um recurso como chave na cache do lado do cliente, pelo que se o URI for alterado, o cliente Web ignora as versões de um recurso colocadas em cache anteriormente e obtém a nova versão.

Gerir a simultaneidade numa cache

Muitas vezes, as caches são estruturadas para serem partilhadas por várias instâncias de uma aplicação. Cada instância da aplicação pode ler e modificar os dados na cache. Por conseguinte, os mesmo problemas de simultaneidade que surjam com qualquer arquivo de dados partilhado também se aplicam a uma 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, pode adotar uma de duas abordagens à simultaneidade:

  • Otimista. Imediatamente antes de atualizar os dados, a aplicação verifica se os dados na cache foram alterados desde que foram obtidos. Se os dados ainda forem os mesmos, a alteração pode ser feita. Caso contrário, a aplicação tem de decidir se pretende atualizá-los. (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 obtém os dados, a aplicação bloqueia-os na 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. Esta abordagem poderá ser adequada para situações em que as colisões sejam mais prováveis, especialmente se uma aplicação atualizar vários itens na cache e tiver de garantir que estas alterações são aplicadas de forma consistente.

Implementar a elevada disponibilidade e escalabilidade e melhorar o desempenho

Evite utilizar uma cache como repositório principal de dados; esta é a função do arquivo de dados original a partir do qual a cache é preenchida. O arquivo de dados original é responsável por assegurar a persistência dos dados.

Tenha cuidado para não introduzir dependências críticas da disponibilidade de um serviço de cache partilhada nas suas soluções. Uma aplicação deve poder continuar a funcionar se o serviço que fornece a cache partilhada não estiver disponível. O aplicativo não deve parar de responder ou falhar enquanto aguarda a retomada do serviço de cache.

Por isso, a aplicação tem de estar preparada para detetar a disponibilidade do serviço de cache e reverter para o arquivo de dados original se a cache não estiver acessível. O padrão Disjuntor é útil para processar este cenário. O serviço que fornece a cache pode ser recuperado e, depois de ficar disponível, a cache pode ser novamente preenchida à medida que os dados são lidos a partir do arquivo de dados original, seguindo uma estratégia como o padrão cache-aside.

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 arquivo de dados estiver a ser recuperado, o arquivo de dados original pode ser sobrecarregado com pedidos de dados, o que resulta em tempos limite e falhas de ligações.

Pondere implementar uma cache local privada em cada instância de uma aplicação, juntamente com a cache partilhada à qual todas as instâncias da aplicação acedem. Quando a aplicação obtém um item, pode verificar primeiro na respetiva cache local, em seguida na cache partilhada e, por último, no arquivo de dados original. A cache local pode ser preenchida com os dados na cache partilhada ou na base de dados se a cache partilhada não estiver disponível.

Esta abordagem requer uma configuração cuidadosa para evitar que a cache local se torne demasiado obsoleta em relação à cache partilhada. No entanto, a cache local funciona como uma memória intermédia se a cache partilhada não estiver acessível. A Figura 3 mostra esta estrutura.

Using a local private cache with a shared cache

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

Para suportar caches grandes que contêm dados de duração relativamente longa, alguns serviços cache fornecem uma opção de elevada disponibilidade que implementa a ativação pós-falha automática se a cache ficar indisponível. Normalmente, esta abordagem envolve a replicação dos dados em cache que estão armazenados num servidor de cache primário para um servidor de cache secundário e a mudança para o servidor secundário se o servidor primário falhar ou se a conectividade se perder.

Para reduzir a latência associada à escrita em vários destinos, a replicação para o servidor secundário poderá ocorrer no modo assíncrono quando os dados são escritos na 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 uma cache partilhada for grande, poderá ser vantajoso criar partições dos dados em cache entre os nós para reduzir as possibilidades de contenção e melhorar a escalabilidade. Muitas caches partilhadas suportam a capacidade de adicionar (e remover) dinamicamente nós e rebalancear os dados entre partições. Esta abordagem poderá envolver clustering, em que a coleção de nós é apresentada às aplicações cliente como uma única cache totalmente integrada. No entanto, internamente, os dados estão dispersos entre nós, seguindo uma estratégia de distribuição predefinida que equilibra a carga de forma uniforme. 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 da cache. Se um nó falhar, o resto da cache continua acessível. O clustering é frequentemente utilizado em conjunto com a replicação e ativação pós-falha. Cada nó pode ser replicado e a réplica pode ser rapidamente colocada online se o nó falhar.

Muitas operações de leitura e escrita são passíveis de envolver objetos ou valores de dados únicos. No entanto, por vezes poderá ser necessário armazenar ou obter rapidamente grandes volumes de dados. Por exemplo, o seeding de uma cache pode implicar escrever centenas ou milhares de itens na cache. Uma aplicação também poderá ter de obter um grande número de itens relacionados da cache como parte do mesmo pedido.

Muitas caches de grande escala fornecem operações de lote para estes fins. Isto permite que uma aplicação cliente coloque um grande volume de itens num pacote num único pedido e reduz a sobrecarga que está associada à realização de um grande número de pequenos pedidos.

Colocação em cache e consistência eventual

Para que o padrão cache-aside funcione, a instância da aplicação que preenche a cache tem de ter acesso à versão mais recente e consistente dos dados. Num sistema que implementa a consistência eventual (por exemplo, um arquivo de dados replicado), este pode não ser o caso.

Uma instância de uma aplicação poderá modificar um item de dados e invalidar a versão em cache desse item. Outra instância da aplicação poderá tentar ler este item a partir de uma cache, o que provoca uma falha de acerto na cache, pelo que lê os dados do arquivo de dados e adiciona-os à 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 o processamento da consistência eventual, veja o Data consistency primer (Manual básico sobre a consistência dos dados).

Proteger os dados em cache

Independentemente do serviço de cache que utilizar, considere como proteger os dados que são mantidos na cache contra o acesso não autorizado. Existem duas preocupações principais:

  • A privacidade dos dados na cache.
  • A privacidade dos dados à medida que fluem entre a cache e a aplicação que está a utilizar a cache.

Para proteger os dados na cache, o serviço de cache pode implementar um mecanismo de autenticação que exige que as aplicações especifiquem o seguinte:

  • As identidades que podem aceder a dados na cache.
  • As operações (leitura e escrita) que estas identidades estão autorizadas a executar.

Para reduzir a sobrecarga associada à leitura e escrita de dados, depois de ser concedido a uma identidade acesso de escrita e/ou leitura à cache, essa identidade pode utilizar quaisquer dados na cache.

Se precisar de restringir o acesso a subconjuntos de dados em cache, pode executar um dos seguintes procedimentos:

  • Divida a cache em partições (utilizando diferentes servidores de cache) e conceda apenas acesso a identidades para as partições que devem estar autorizadas a utilizar.
  • Encripte os dados em cada subconjunto utilizando chaves diferentes e forneça as chaves de encriptação apenas às identidades que devem ter acesso a cada subconjunto. Uma aplicação cliente poderá continuar a poder obter todos os dados na cache, mas só poderá desencriptar os dados para os quais tem as chaves.

Também terá de proteger os dados à medida que entram e saem da cache. Para tal, depende das funcionalidades de segurança fornecidas pela infraestrutura de rede que as aplicações cliente utilizam para ligar à cache. Se a cache for implementada com um servidor no local dentro da mesma organização que aloja as aplicações cliente, o isolamento da própria rede poderá não exigir que execute passos adicionais. Se a cache estiver localizada remotamente e precisar de uma ligação TCP ou HTTP através de uma rede pública (como a Internet), considere a implementação de 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. Fornece um serviço de colocação em cache que pode ser acedido a partir de qualquer aplicação do Azure, quer a aplicação esteja implementada como um serviço cloud, um site ou dentro de uma máquina virtual do Azure. As caches podem ser partilhadas por aplicações cliente que tenham a chave de acesso adequada.

O Cache Redis do Azure é uma solução de cache de alto desempenho que fornece disponibilidade, escalabilidade e segurança. Normalmente, é executada como um serviço distribuído por uma ou mais máquinas dedicadas. Tenta armazenar a maior quantidade de informações possível na memória para garantir o acesso rápido. Esta arquitetura visa fornecer latência baixa e débito elevado ao reduzir 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.

Funcionalidades de Redis

O Redis é mais do que um simples servidor de cache. Fornece uma base de dados dentro da memória distribuída com um vasto conjunto de comandos que suporta muitos cenários comuns. Estes são descritos mais à frente neste documento, na secção Utilizar a colocação em cache de Redis. Esta secção resume algumas das principais funcionalidades fornecidas pelo Redis.

Redis como uma base de dados dentro da memória

O Redis suporta operações de leitura e escrita. No Redis, as escritas podem ser protegidas de uma falha de sistema ao serem periodicamente armazenadas num ficheiro de instantâneo local ou num ficheiro de registo só de 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, lê os dados a partir do ficheiro de registo ou instantâneo e utiliza-os para construir a cache dentro da memória. Para obter mais informações, veja Redis persistence (Persistência de Redis) no site do Redis.

Nota

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 de Redis

O Redis é um arquivo de chave-valor onde os valores podem conter tipos simples ou estruturas de dados complexas, tais como hashes, listas e conjuntos. Suporta um conjunto de operações atómicas nestes tipos de dados. As chaves podem ser permanentes ou marcadas com um TTL limitado, altura em que a chave e o seu valor correspondente são automaticamente removidos da cache. Para obter mais informações sobre as chaves e valores de Redis, visite a página An introduction to Redis data types and abstractions (Uma introdução aos tipos de dados e abstrações de Redis) no site do Redis.

Replicação e clustering de 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 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 Replication (Replicação) no site do Redis.

O Redis também fornece clustering, que lhe permite particionar dados de forma transparente em fragmentos entre servidores e distribuir a carga. Esta funcionalidade melhora a escalabilidade porque podem ser adicionados novos servidores Redis e podem ser criadas novas partições dos dados à medida que o tamanho da cache aumenta.

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

Utilização de memória do Redis

Uma cache de Redis tem um tamanho finito que depende dos recursos disponíveis no computador anfitrião. Quando configura um servidor Redis, pode especificar a quantidade máxima de memória que este pode utilizar. 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. Esta funcionalidade pode ajudar a impedir que a cache dentro da memória seja preenchida com dados antigos ou obsoletos.

À medida que a memória é preenchida, o Redis pode automaticamente expulsar chaves e os respetivos valores ao seguir 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 Using Redis as an LRU cache (Utilizar o Redis como uma cache LRU) fornece mais informações.

Transações e lotes de Redis

O Redis permite que uma aplicação cliente submeta uma série de operações que leem e escrevem dados na cache como uma transação atómica. Todos os comandos na transação têm a garantia de ser executados sequencialmente e nenhum comando emitido por outros clientes simultâneos será interligado 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 duas fases: a primeira é quando os comandos são colocados em fila e a segunda é quando os comandos são executados. Durante a fase de colocação de comandos em fila, os comandos que compõem a transação são submetidos pelo cliente. Se ocorrer algum tipo de erro neste momento (por exemplo, um erro de sintaxe ou um número incorreto de parâmetros), o Redis recusa-se a processar toda a transação e elimina-a.

Durante a fase de execução, o Redis executa cada comando em fila na 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 a evitar problemas de desempenho causados por 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 o bloqueio com Redis, visite a página Transactions (Transações) no site do Redis.

O Redis também suporta lotes não transacionais de solicitações. O protocolo de Redis que os clientes utilizam para enviar comandos para um servidor Redis permite que um cliente envie uma série de operações como parte do mesmo pedido. Isto 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 de Redis

O Redis concentra-se puramente em fornecer acesso rápido aos dados e foi concebido para ser executado dentro de um ambiente fidedigno, que pode ser acedido apenas por clientes fidedignos. O Redis suporta um modelo de segurança limitada com base na autenticação por palavra-passe. (É possível remover completamente a autenticação, embora não recomendemos isso.)

Todos os clientes autenticados partilham a mesma palavra-passe global e têm acesso aos mesmos recursos. Se precisar de segurança de início de sessão mais completa, tem de implementar a sua própria camada de segurança no servidor Redis e todos os pedidos de cliente devem passar por esta camada adicional. O Redis não deve ser exposto diretamente a clientes não confiáveis ou não autenticados.

Pode restringir o acesso aos comandos ao desativá-los ou mudar o nome dos mesmos (e ao fornecer os novos nomes apenas a clientes com privilégios).

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 precisar de proteger os dados à medida que fluem através da rede, recomendamos a implementação de um proxy SSL.

Para obter mais informações, visite a página Redis security (Segurança de Redis) no site do Redis.

Nota

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 de Redis do Azure

O Cache Redis do Azure fornece acesso aos servidores Redis hospedados em um datacenter do Azure. Funciona como uma fachada que fornece controlo de acesso e segurança. Pode aprovisionar uma cache através do portal do Azure.

O portal fornece diversas configurações predefinidas. Estas variam desde uma cache de 53 GB em execução como um serviço dedicado que suporta comunicações SSL (para privacidade) e replicação mestre/subordinada com um SLA de 99,9% de disponibilidade, até uma cache de 250 MB sem replicação (sem garantias de disponibilidade) em execução em hardware partilhado.

Com o portal do Azure, também pode configurar a política de expulsão da cache e controlar o acesso à cache ao adicionar utilizadores às funções fornecidas. Estas funções, que definem as operações que os membros podem executar, incluem Proprietário, Contribuidor e Leitor. Por exemplo, os membros da função Proprietário têm controlo total sobre a cache (incluindo segurança) e os respetivos conteúdos, os membros da função Contribuidor podem ler e escrever informações na cache e os membros da função Leitor só podem obter dados da cache.

A maioria das tarefas administrativas são realizadas 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 visualização gráfica conveniente que lhe permite monitorizar o desempenho da cache. Por exemplo, pode ver o número de ligações que estão a ser estabelecidas, o número de pedidos que estão a ser realizados, o volume de leituras e escritas, e o número de acertos na cache por oposição às falhas de acerto na cache. Com estas informações, pode determinar a eficácia da cache e, se for necessário, mudar para uma configuração diferente ou alterar a política de expulsão.

Além disso, pode criar alertas que enviam mensagens de e-mail a um administrador se uma ou mais métricas críticas estiverem fora de um intervalo esperado. Por exemplo, poderá querer alertar um administrador se o número de falhas de acerto na cache exceder um valor especificado na última hora, porque significa que a cache poderá ser demasiado pequena ou os dados poderão estar a ser expulsos de forma demasiado rápida.

Também pode monitorizar a CPU, memória e utilização da rede para a 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.

Colocação em cache do estado de sessão e da 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:

  • Partilha do estado de sessão com um grande número de instâncias de aplicações Web ASP.NET.
  • Fornecimento de escalabilidade melhorada.
  • Suporte de acesso simultâneo controlado aos mesmos dados de estado de sessão para vários leitores e um único escritor.
  • Utilização da compressão para poupar 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.

Nota

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 do acesso à cache de fora do Azure pode eliminar os benefícios de desempenho da colocação 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.

Criar uma cache de Redis personalizada

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 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 datacenters diferentes do Azure, localizados em regiões diferentes, se pretender localizar os dados em cache próximos das aplicações que têm maior probabilidade de os utilizar. Para ver um exemplo de criação e configuração de um nó de Redis em execução como uma VM do Azure, veja Running Redis on a CentOS Linux VM in Azure (Executar o Redis numa VM Linux CentOS no Azure).

Nota

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

Criação de partições de uma cache de Redis

A criação de partições da cache envolve a divisão da cache em vários computadores. Esta estrutura oferece várias vantagens em relação à utilização de um único servidor de cache, incluindo:

  • Criação de uma cache que é muito maior do que a que pode ser armazenada num único servidor.
  • Distribuição de dados entre servidores, melhorando a disponibilidade. Se um servidor falhar ou ficar inacessível, os dados que contém não estão disponíveis, mas continua a ser possível aceder aos dados nos restantes servidores. 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 em cache num servidor que fique inacessível podem ser colocados em cache num servidor diferente.
  • Distribuição da carga entre servidores, melhorando assim o desempenho e a escalabilidade.
  • Geolocalização dos dados perto dos utilizadores que acedem aos mesmos, reduzindo assim a latência.

Para uma cache, a forma mais comum de criação de partições é a fragmentação. Nesta estratégia, cada partição (ou fragmento) é uma cache de Redis por direito próprio. Os dados são direcionados para uma partição específica, utilizando a lógica de fragmentação, que pode utilizar várias abordagens para distribuir os dados. O artigo Padrão de fragmentação fornece mais informações sobre a implementação da fragmentação.

Para implementar a criação de partições numa cache de Redis, pode adotar uma das abordagens seguintes:

  • Encaminhamento de consulta do lado do servidor. Nesta técnica, uma aplicação cliente envia um pedido para qualquer um dos servidores Redis que compõem a cache (provavelmente, o servidor mais próximo). Cada servidor Redis armazena os metadados que descrevem a partição que contém e também inclui informações sobre as partições que estão localizadas noutros servidores. O servidor Redis examina o pedido de cliente. Se puder ser resolvido localmente, irá executar a operação pedida. Caso contrário, reencaminhará o pedido para o servidor adequado. Este modelo é implementado pelo clustering de Redis e está descrito com maior detalhe na página Redis cluster tutorial (Tutorial do cluster Redis) no site do Redis. O clustering de Redis é transparente para as aplicações cliente e podem ser adicionados servidores Redis adicionais ao cluster (e os dados podem ser particionados novamente) sem ser preciso que reconfigure os clientes.
  • Criação de partições do lado do cliente. Neste modelo, a aplicação cliente contém a lógica (possivelmente sob a forma de uma biblioteca) que encaminha os pedidos para o servidor Redis adequado. 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.
  • Criação de partições assistida por proxy. Neste esquema, as aplicações cliente enviam pedidos para um serviço de proxy intermediário que compreende como os dados estão particionados e, em seguida, encaminha o pedido para o servidor Redis adequado. 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. Esta abordagem requer um nível adicional de complexidade para implementar o serviço e os pedidos poderão demorar mais tempo a serem executados do que com a criação de partições do lado do cliente.

A página Partitioning: how to split data among multiple Redis instances (Criação de partições: como dividir dados entre várias instâncias de Redis), no site do Redis, fornece mais informações sobre a implementação da criação de partições com o Redis.

Implementar aplicações cliente da cache de 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 resume os detalhes da ligação a um servidor Redis, do envio de comandos e da receção de 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 ligar a um servidor Redis, utilize o método Connect estático da classe ConnectionMultiplexer. A ligação que este método cria foi concebida para ser utilizada ao longo da duração da aplicação cliente e a mesma ligação pode ser utilizada por vários threads em simultâneo. Não reconecte e desconecte cada vez que executar uma operação Redis, pois isso pode prejudicar o desempenho.

Pode especificar os parâmetros de ligação, tais como o endereço do anfitrião de Redis e a palavra-passe. 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 ligar ao servidor Redis, pode obter um identificador na base de dados Redis que funciona como a cache. A ligação de Redis fornece o método GetDatabase para fazê-lo. Em seguida, pode obter itens da cache e armazenar dados na cache utilizando os métodos StringGet e StringSet. Estes métodos esperam uma chave como parâmetro e devolvem o item na cache que tem um valor correspondente (StringGet) ou adicionam o item à cache com esta chave (StringSet).

Consoante a localização do servidor Redis, muitas operações podem estar sujeitas a alguma latência enquanto um pedido é transmitido para o servidor e uma resposta é devolvida ao cliente. A biblioteca StackExchange fornece versões assíncronas de muitos dos métodos que expõe para ajudar as aplicações cliente a manterem-se reativas. Esses métodos suportam o padrão assíncrono baseado em tarefas no .NET Framework.

O fragmento de código seguinte mostra um método denominado RetrieveItem. Ilustra uma implementação do padrão cache-aside com base no Redis e na biblioteca StackExchange. O método assume um valor de chave de cadeia e tenta obter o item correspondente da cache de Redis, chamando o método StringGetAsync (a versão assíncrona de 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, é adicionado à cache através do método StringSetAsync, para que possa ser obtido 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. Podem aceitar qualquer item que esteja serializado como uma matriz de bytes. Se precisar de guardar um objeto .NET, pode serializá-lo como um fluxo de bytes e utilizar o método StringSet para gravá-lo na cache.

Da mesma forma, pode ler um objeto da cache através do método StringGet e anular a serialização do mesmo como um objeto .NET. O código seguinte mostra um conjunto de métodos de extensão para a interface IDatabase (o método GetDatabase de uma ligação de Redis devolve um objeto IDatabase) e algum código de exemplo que utiliza estes métodos para ler e escrever um objeto BlogPost na 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 seguinte ilustra um método denominado RetrieveBlogPost que utiliza estes métodos de extensão para ler e escrever um objeto BlogPost serializável na 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 suporta o pipelining de comandos se uma aplicação cliente enviar vários pedidos assíncronos. O Redis pode executar a multiplexação dos pedidos com a mesma ligação em vez de receber e responder a comandos numa sequência estrita.

Esta abordagem ajuda a reduzir a latência, fazendo uma utilização mais eficiente da rede. O fragmento de código seguinte mostra um exemplo que obtém os detalhes de dois clientes em simultâneo. O código submete dois pedidos e, em seguida, executa mais algum processamento (não apresentado) antes de ficar à espera de receber os resultados. O método Wait do objeto de cache é semelhante ao método Task.Wait do .NET Framework:

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. Também estão disponíveis mais informações em StackExchange.Redis.

A página Pipelines and multiplexers (Pipelines e multiplexadores), no mesmo site, fornece mais informações sobre operações assíncronas e pipelines com o Redis e a biblioteca StackExchange.

Utilizar a colocação em cache de Redis

A utilização mais simples do Redis para questões de colocação em cache consiste nos pares chave-valor, em que o valor é uma cadeia 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). Este cenário foi ilustrado na secção Implementar aplicações cliente da cache de Redis mais atrás neste artigo.

Tenha em atenção que as chaves também contêm dados não interpretados, pelo que pode utilizar informações binárias como a chave. No entanto, quanto mais longa for a chave, mais espaço de armazenamento ocupará, e mais tempo demorará a executar operações de pesquisa. Por questões de capacidade de utilização e facilidade de manutenção, estruture cuidadosamente o seu espaço de chaves e utilize chaves explicativas (mas não verbosas).

Por exemplo, utilize chaves estruturadas, tais como o "cliente:100" para representar a chave para o cliente com o ID 100 em vez de simplesmente "100". Este esquema permite facilmente fazer a distinção entre os valores que armazenam diferentes tipos de dados. Por exemplo, também podia utilizar a chave "pedidos:100" para representar a chave para o pedido com o ID 100.

Para além das cadeias binárias unidimensionais, um valor num par chave-valor de Redis também pode conter informações mais estruturadas, incluindo listas, conjuntos (ordenados e não ordenados) e hashes. O Redis fornece um conjunto de comandos abrangente que pode manipular estes tipos e muitos destes comandos estão disponíveis para aplicações .NET Framework através de uma biblioteca de cliente, como StackExchange. A página An introduction to Redis data types and abstractions (Uma introdução aos tipos de dados e abstrações de Redis), no site do Redis, fornece uma descrição geral mais detalhada destes tipos e dos comandos que pode utilizar para manipulá-los.

Esta secção resume alguns casos de utilização comuns para estes tipos de dados e comandos.

Executar operações atómicas e de lote

O Redis suporta uma série de operações get e set atómicas em valores de cadeia. Estas operações eliminam os possíveis riscos de race que poderão ocorrer ao utilizar comandos GET e SET separados. As operações que estão disponíveis incluem:

  • INCR, INCRBY, DECR e DECRBY, que executam operações atómicas de incremento e diminuição em valores de dados numéricos inteiros. A biblioteca StackExchange fornece versões sobrecarregadas dos métodos IDatabase.StringIncrementAsync e IDatabase.StringDecrementAsync para executar estas operações e devolver o valor resultante que é armazenado na cache. O fragmento de código seguinte ilustra como utilizar estes 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 obtém o valor que está associada a uma chave e altera-o para um novo valor. A biblioteca StackExchange disponibiliza esta operação através do método IDatabase.StringGetSetAsync. O fragmento de código abaixo mostra um exemplo deste método. Este código devolve o valor atual que está associado à chave "data:counter" do exemplo anterior. Em seguida, repõe o valor para esta chave a zero, tudo como parte da mesma operação:

    ConnectionMultiplexer redisHostConnection = ...;
    IDatabase cache = redisHostConnection.GetDatabase();
    ...
    string oldValue = await cache.StringGetSetAsync("data:counter", 0);
    
  • MGETe MSET, que podem devolver ou alterar um conjunto de valores de cadeia como uma única operação. Os métodos IDatabase.StringGetAsync e IDatabase.StringSetAsync são sobrecarregados para suportar esta funcionalidade, conforme mostra o exemplo seguinte:

    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);
    
    

Também pode combinar várias operações numa única transação de Redis, tal como descrito na secção Transações e lotes de Redis mais atrás neste no artigo. A biblioteca StackExchange fornece suporte para transações através da interface ITransaction.

Para criar um objeto ITransaction, utilize o método IDatabase.CreateTransaction. Para invocar comandos para a transação, utilize os métodos fornecidos pelo objeto ITransaction.

A interface ITransaction proporciona acesso a um conjunto de métodos semelhantes aos métodos acedidos pela interface IDatabase, com a exceção de 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 devolvido pelo método ITransaction.Execute indica se a transação foi criada com êxito (true) ou se falhou (false).

O fragmento de código seguinte 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 que as transações de Redis diferentes das transações em bases de dados relacionais. O método Execute coloca, simplesmente, em fila todos os comandos que compõem a transação a executar e, se qualquer um deles tiver um formato incorreto, a transação é parada. Se todos os comandos tiverem sido colocados em fila com êxito, cada comando é executado no modo assíncrono.

Se qualquer um dos comandos falhar, os outros continuam a ser processados. Se precisar de verificar se um comando foi concluído com êxito, terá de obter os resultados do comando através da propriedade Resultado da tarefa correspondente, tal como mostra o exemplo acima. A leitura da propriedade Resultado irá bloquear o thread de chamada até que a tarefa seja concluída.

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

Quando executar operações de lote, pode utilizar a interface IBatch da biblioteca StackExchange. A interface proporciona acesso a um conjunto de métodos semelhantes aos métodos acedidos pela interface IDatabase, com a exceção de que todos os métodos são assíncronos.

Para criar um objeto IBatch, tem de utilizar o método IDatabase.CreateBatch e, em seguida, executar o lote através do comando IBatch.Execute, tal como mostra o exemplo seguinte. Este código define, simplesmente, um valor de cadeia, incrementa e diminui os mesmos contadores utilizados no exemplo anterior e apresenta 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.

Executar operações de cache «fire and forget»

O Redis suporta operações «fire and forget», utilizando 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 «fire and forget»:

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

Especificar chaves que expiram automaticamente

Quando armazena um item numa cache de Redis, pode especificar um tempo limite após o qual o item será automaticamente removido da cache. Também pode consultar a quantidade de tempo que uma chave tem até expirar, utilizando o comando TTL. Este comando está disponível para aplicações StackExchange através do método IDatabase.KeyTimeToLive.

O fragmento de código seguinte mostra como definir um prazo de expiração de 20 segundos numa chave e consultar a duração 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");

Também pode definir o prazo de expiração como uma data e hora específicas, utilizando o comando EXPIRE, que está disponível na biblioteca StackExchange como o método KeyExpireAsync:

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));
...

Gorjeta

Pode remover manualmente um item da cache através do comando DEL, que está disponível através da biblioteca StackExchange como o método IDatabase.KeyDeleteAsync.

Utilizar etiquetas para fazer a correlação cruzada de itens em cache

Um conjunto de Redis é uma coleção de vários itens que partilham uma chave única. Pode criar um conjunto, utilizando o comando SADD. Pode obter os itens de um conjunto, utilizando o comando SMEMBERS. A biblioteca StackExchange implementa o comando SADD com o método IDatabase.SetAddAsync e o comando SMEMBERS com o método IDatabase.SetMembersAsync.

Também pode combinar conjuntos existentes para criar novos conjuntos através dos comandos SDIFF (diferença do conjunto), SINTER (interseção do conjunto) e SUNION (união do conjunto). A biblioteca StackExchange unifica estas operações no método IDatabase.SetCombineAsync. O primeiro parâmetro deste método especifica a operação de conjunto a executar.

Os fragmentos de código seguintes mostram como os conjuntos podem ser úteis para armazenar e obter rapidamente coleções de itens relacionados. Este código utiliza o tipo BlogPost que foi descrito na secção Implementar aplicações cliente da cache de Redis, mais atrás neste artigo.

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

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
}

Pode armazenar as etiquetas para cada objeto BlogPost como um conjunto numa cache de Redis e associar cada conjunto ao ID do BlogPost. Isto permite que uma aplicação localize rapidamente todas as etiquetas que pertencem a uma mensagem de blogue específica. Para ativar a pesquisa na direção oposta e localizar todas as mensagens de blogue que partilham uma etiqueta específica, pode criar outro conjunto que contenha as mensagens de blogue que fazem referência ao ID de etiqueta 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);
    }
}

Estas estruturas permitem realizar muitas consultas comuns de forma muito eficiente. Por exemplo, pode localizar e apresentar todas as etiquetas para a mensagem de blogue 1 desta forma:

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

Pode localizar todas as etiquetas que são comuns à mensagem de blogue 1 e à mensagem de blogue 2, executando uma operação de interseção do conjunto, da seguinte forma:

// 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 pode localizar todas as mensagens de blogue que contêm uma etiqueta 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 acedidos recentemente

Uma tarefa comum necessária de muitas aplicações é localizar os itens acedidos mais recentemente. Por exemplo, um site de blogging poderá querer apresentar informações sobre as mensagens de blogue lidas mais recentemente.

Pode implementar esta funcionalidade através de uma lista de Redis. Uma lista de Redis contém vários itens que partilham a mesma chave. A lista funciona como uma fila de extremidade dupla. Pode enviar por push itens para ambas as extremidade da lista através dos comandos LPUSH (push à esquerda) e RPUSH (push à direita). Pode obter itens de ambas as extremidades da lista através dos comandos LPOP e RPOP. Também pode devolver um conjunto de elementos através dos comandos LRANGE e RRANGE.

Os fragmentos de código abaixo mostram como pode executar estas operações através da biblioteca StackExchange. Este código utiliza o tipo BlogPost dos exemplos anteriores. À medida que uma mensagem de blogue é lida por um utilizador, o método IDatabase.ListLeftPushAsync envia por push o título da mensagem de blogue para uma lista que está associada à chave "blog:recent_posts" na cache de 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 são lidas mais mensagens de blogue, os respetivos títulos são enviados por push 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 mensagem de blogue for lida mais do que uma vez, terá várias entradas na lista.)

Pode apresentar os títulos das mensagens lidas mais recentemente através do método IDatabase.ListRange. Este método aceita a chave que contém a lista, um ponto de partida e um ponto de fim. O código seguinte obtém os títulos das 10 mensagens de blogue (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 tal, pode utilizar os métodos IDatabase.ListLeftPopAsync e IDatabase.ListRightPopAsync.

Para impedir que a lista cresça indefinidamente, pode eliminar periodicamente itens ao cortar a lista. O fragmento de código abaixo mostra como remover todos os itens, exceto os cinco itens mais à esquerda, da lista:

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

Implementar um quadro líder

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

O fragmento de código seguinte adiciona o título de uma mensagem de blogue a uma lista ordenada. Neste exemplo, cada mensagem de blogue também tem um campo de pontuação que contém a classificação da mensagem de blogue.

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);

Pode obter os títulos e as pontuações da mensagem de blogue por ordem ascendente de pontuação através do comando IDatabase.SortedSetRangeByRankWithScores:

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

Nota

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.

Também pode obter itens por ordem descendente de pontuações e limitar o número de itens que são devolvidos ao fornecer parâmetros adicionais para o método IDatabase.SortedSetRangeByRankWithScoresAsync. O exemplo seguinte mostra os títulos e as pontuações das 10 mensagens de blogue com melhor classificação:

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

O exemplo seguinte utiliza o método IDatabase.SortedSetRangeByScoreWithScoresAsync, que pode utilizar para limitar os itens que são devolvidos àqueles que se inserem num 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 através de canais

Para além de funcionar como uma cache de dados, um servidor Redis fornece mensagens através de um mecanismo publicador/subscritor de elevado desempenho. As aplicações cliente podem subscrever um canal e os outros serviços ou aplicações podem publicar mensagens no canal. As aplicações de subscrição recebem, em seguida, estas mensagens e podem processá-las.

O Redis fornece o comando SUBSCRIBE para as aplicações cliente utilizarem para subscrever canais. Este comando espera o nome de um ou mais canais nos quais a aplicação irá aceitar mensagens. A biblioteca StackExchange inclui a interfaceISubscription, que permite que uma aplicação .NET Framework subscreva e publique em canais.

Para criar um objeto ISubscription, utilize o método GetSubscriber da ligação ao servidor Redis. Em seguida, para escutar as mensagens num canal, utilize o método SubscribeAsync deste objeto. O exemplo de código seguinte mostra como subscrever 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 método Subscribe é o nome do canal. Este nome segue-se as mesmas convenções que são utilizadas pelas chaves na 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.

Tenha também em atenção que o espaço de nomes utilizado pelos canais é distinto daquele utilizado pelas chaves. Isto significa que pode ter canais e chaves com o mesmo nome, embora isto possa dificultar a manutenção do código da sua aplicação.

O segundo parâmetro é um delegado de ação. Este delegado é executado no modo assíncrono sempre que uma nova mensagem for apresentada no canal. Este exemplo apresenta simplesmente a mensagem na consola (a mensagem incluirá o título de uma mensagem de blogue).

Para publicar num canal, uma aplicação pode utilizar o comando PUBLISH do Redis. A biblioteca StackExchange fornece o método IServer.PublishAsync para executar esta operação. O fragmento de código seguinte 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 deve compreender sobre o mecanismo de publicação/subscrição:

  • 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, assim que 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. Num sistema muito ativo com um grande número de mensagens e muitos subscritores e publicadores, a entrega sequencial garantida de mensagens pode abrandar o desempenho do sistema. Se cada mensagem for independente e a ordem não for importante, pode ativar o processamento simultâneo pelo sistema de Redis, o que pode ajudar a melhorar a capacidade de resposta. Poderá fazê-lo num cliente StackExchange, definindo PreserveAsyncOrder da ligação utilizada pelo subscritor como false:
ConnectionMultiplexer redisHostConnection = ...;
redisHostConnection.PreserveAsyncOrder = false;
ISubscriber subscriber = redisHostConnection.GetSubscriber();

Considerações sobre serialização

Quando seleciona um formato de serialização, considere um compromisso entre desempenho, interoperabilidade, controlo de versões, compatibilidade com sistemas existentes, compressão de dados e sobrecarga de memória. Ao avaliar o desempenho, lembre-se de que os benchmarks são altamente dependentes do contexto. Podem não refletir a 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 chamadas protobuf) é um formato de serialização desenvolvido pela Google para serializar dados estruturados de forma eficiente. Ele usa arquivos de definição fortemente tipados para definir estruturas de mensagem. Em seguida, estes ficheiros de definição são compilados em código específico da linguagem para serializar e anular a serialização de mensagens. O protobuf pode ser utilizado sobre os mecanismos RPC existentes ou pode gerar um serviço RPC.

  • Apache Thrift utiliza uma abordagem semelhante, com ficheiros de definição com tipos de dados inflexíveis e um passo de compilação para gerar o código de serialização e os serviços RPC.

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

  • JSON é um padrão aberto que utiliza campos de texto legíveis por humanos. Possui um amplo suporte em várias plataformas. JSON não usa esquemas de mensagem. Sendo um formato baseado em texto, não é muito eficiente através do fio. No entanto, em alguns casos, pode estar a devolver itens em cache diretamente para um cliente através de HTTP, caso este em que armazenar o JSON podia poupar o custo de anular a serialização de outro formato e, em seguida, serializar para JSON.

  • BSON é um formato de serialização binário que utiliza uma estrutura semelhante à do JSON. O BSON foi concebido para ser simples, fácil de analisar e rápido de serializar e anular a serialização, relativamente ao JSON. As payloads são comparáveis em termos de tamanho com o JSON. Consoante os dados, uma payload de BSON pode ser menor ou maior do que uma payload de JSON. 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 que foi concebido para ser compacto para transmissão por fio. Não existem esquemas de mensagem ou verificação de tipo de mensagem.

  • Bond é uma estrutura multi-plataforma para trabalhar com dados esquematizados. Suporta a serialização e a anulação da serialização em diferentes linguagens. As diferenças relevantes em relação a outros sistemas aqui listados são o suporte de herança, aliases de tipo e genéricos.

  • gRPC é um sistema RPC de código aberto desenvolvido pelo Google. Por predefinição, utiliza Protocol Buffers como linguagem de definição e formato de intercâmbio de mensagem subjacente.

Próximos passos

Os padrões a seguir também podem ser relevantes para seu cenário quando você implementa o cache em seus aplicativos:

  • Padrão cache-aside: este padrão descreve como carregar dados a pedido para uma cache a partir de um arquivo de dados. Este padrão também ajuda a manter a consistência entre os dados mantidos na cache e os dados do arquivo de dados original.

  • O Padrão de fragmentação fornece informações sobre a implementação da criação de partições horizontais para ajudar a melhorar a escalabilidade ao armazenar e aceder a grandes volumes de dados.