“O cache na memória usa memória do servidor para armazenar dados armazenados em cache. Esse tipo de cache é adequado para um único servidor ou vários servidores usando afinidade de sessão. A afinidade da sessão também é conhecida como “Sticky Sessions”. A afinidade da sessão significa que as solicitações de um cliente são sempre encaminhadas para o mesmo servidor para processamento.”

- Microsoft

Existem pelo menos dois tipos de cache:

 

  • Cache de memória: amplamente utilizado, considerado o mais simples e muito eficaz. Aumenta significativamente o desempenho da aplicação;
  • Cache distribuído: é compartilhado por vários servidores e pode armazenar dados em servidores que não compartilham a mesma memória. É utilizado normalmente em cenários de maior escala e provisionamento automático de recursos.

 

Vou falar um pouco hoje sobre “Cache em Memória”, apontar vantagens e desvantagens, como implementar e quais são os cenários apropriados para sua utilização.

 

O Cache em memória é comumente utilizado para cenários onde há pouca mutabilidade de informações retornadas de um servidor de aplicação. Tudo aquilo que tem pouca alteração pode ser um ótimo candidato a adicionar o dado em cache. A ideia central é fazer com que a informação requisitada, por exemplo, de uma aplicação web/mobile chegue até a camada de backend e não precise ir todas as vezes a uma fonte de dados obter a mesma informação. Basicamente, o cache fica entre a aplicação e nossa fonte de dados.

 

Essa é uma ótima opção para determinados pontos de nossa aplicação que podem ser consumidas com frequência, mas que geralmente não serão alteradas sempre, por exemplo:

 

  • Categoria de produtos
  • Distritos
  • Conselhos
  • Freguesias
  • Estado Civil
  • Unidades de Medidas

 

O processo de requisição ocorre da primeira vez obtendo normalmente o dado na fonte de dados. Porém, antes de dar a resposta, adicionamos o dado em memória referenciando ele por meio de uma chave única e um tempo de expiração. Estas são as características básicas de um cache:

 

  • Chave Única que identifica em memória e nos permite recuperar o dado;
  • O Dado propriamente dito que queremos guardar em memória;
  • Tempo de Expiração é o tempo que este dado ficará disponível em memória.

 

VANTAGENS

Existem inúmeras vantagens. No meu ponto de vista, as principais são:

  • Economizar recursos consumidos por uma aplicação na nuvem quando não há a necessidade de sobrecarregar a fonte de dados em todas as requisições;
  • Melhora significativamente a experiência do cliente devido a resposta muito rápida das requisições;
  • Confiabilidade nos dados retornados;
  • Reduz as idas a fonte de dados.

 

DESVANTAGENS

  • Os dados podem não estar atualizados até que o tempo de expiração tenha ocorrido;
  • Aumento de manutenção;
  • Possíveis problemas de escalabilidade.

 

O uso deve ser muito bem pensado do ponto de vista de regras de negócio. Eu já fiz utilização de cache em memória para aplicações onde praticamente todos os endpoints eram colocados em memória, porém era um cenário onde havia uma atualização diária de dados na parte da noite. Os dados eram, de fato, imutáveis quase 24 horas do dia. Eram milhões de dados para servir gráficos e KPI’s, portanto não tenha medo de fazer uso do cache em memória, desde que isso faça sentido para o seu projeto.

 

POLÍTICAS DE CACHE

Como citei anteriormente, é muito fácil fazer a implementação de cache em memória no .NET, porém é mais fácil ainda cometer erros. Quando você precisa escalar uma aplicação onde há implementação de cache, nada garante que o dados que ficaram guardados em memória no momento de uma requisição será o mesmo para as demais requisições, pois é possível que o servidor “A” da requisição anterior não seja o mesmo das outras, colocando novamente o dado em cache, só que agora no servidor “B”. Se este for o seu cenário, é importante pensar em cache distribuído. Existem opções como o “Redis que resolvem este problema.

 

Ainda sobre memória, é importante definir algumas estratégias para contornar problemas. Imagina a situação onde uma requisição retorna uma lista de unidades de medidas: você vai colocar na memória e vai retornar rapidamente a lista com as dez unidades de medidas, onde o tempo de expiração é de 12 horas. Este é o cenário perfeito pois segundo a informação que o cliente passou é um tipo de dado pouco atualizado. No entanto, antes de as 12 horas expirarem alguém foi ao backoffice e incluiu mais uma unidade de medida, porém ainda faltariam 8 horas para sua expiração. Não é nada confortável para um cliente aguardar tanto tempo para ver a lista de unidades de medidas atualizada e disponível. Possivelmente ele precisa desta informação para dar andamento em outros processos importantes.

 

Neste momento devemos pensar em soluções de como resolver e usar bom senso para atender a performance, mas não deixar o cliente sem as suas valiosas informações. Para isso podemos implementar políticas de cache onde provisionamos cenários importantes e adequamos a nossa implementação para atender possíveis questões como esta que eu citei. Finalmente a solução pode ser uma abordagem onde o endpoint de criação desta nova unidade de medida, quando consumido, vai verificar a existência da lista em memória e limpar por meio de sua chave única. Desta forma, a próxima requisição para listagem de unidades de medidas irá retornar onze unidades de medidas e não mais as dez anteriores, sem precisar aguardar o tempo de expiração previamente configurado.

 

As principais políticas são as seguintes:

  • Expiração absoluta determina que o dado é excluído somente após a sua expiração;
  • Expiração deslizante determina que o item será removido da memória após não ser acedido por um tempo determinado;
  • Limite de tamanho determina que o tamanho de cache que pode ser utilizado é provisionado ao valor configurado.

Chega de falar! Mãos a obra!!!

 

Vamos criar um exemplo de uma API com diversas operações, onde o objetivo é explorar a listagem de clientes, bem como criação e até remoção. Vamos também simular possíveis problemas e soluções utilizando cache em memória.

 

1 – Vamos criar um projeto de API.

 

Picture18

 

2 – Selecione o tipo de projeto ASP.NET Core Web API.

 

Picture19

 

3 – Informe um nome para sua solução.

 

Picture20

 

4 – Selecione a versão .NET 6.0 LTS e mantenha demais opções como mostra a imagem. Clique em Criar.

 

Picture21

 

5 – Esta é a estrutura básica da sua API.

 

Picture1-Oct-28-2022-10-25-31-3890-AM

 

5A - Precisamos instalar alguns pacotes. Para isso execute no seu package manager console os comandos:

  • Install-Package Microsoft.EntityFrameworkCore
  • Install-Package Microsoft.EntityFrameworkCore.InMemory

 

Picture2-3

 

6 – Vou excluir os arquivos “WeatherForecastController.cs” e “WeatherForecast.cs” e criar as seguintes pastas

 

Picture3-3

 

7 – Dentro da pasta Model vamos criar uma classe chamada “Customer”. Ela será logo mais utilizada para criar uma lista de Customers. É uma classe que possui apenas Id e Name, levando em consideração que é somente um exemplo para falarmos de cache. Esta classe recebe em seu construtor a quantidade de registos que desejamos criar para que seja retornado uma lista de Customers. Para simular os dados eu estou criando aleatoriamente um Id por meio de um Guid e concatenando um nome com o índice do laço de repetição.

 

Picture4-3

 

8 – Vamos utilizar para este projeto o uso de banco de dados em memória (Atenção: não tem relação com memory cache) para não precisar criar um banco de dados real. Isso aumentaria a complexidade e não é a ideia deste artigo. Não deem atenção para padrões e nomenclaturas. O exemplo tem objetivo didático, portanto nome de rotas, classes e métodos são meros exemplos para contextualizar a ideia principal, que é o uso de cache em uma API.

 

Vamos criar um arquivo dentro da pasta Data chamado “MemoryDbContext.cs” e esta classe deverá ficar da seguinte forma:

 

Picture5-3

 

9 – Na pasta Interfaces vamos criar o nosso contrato com o nome “ICustomerService.cs”, que será o responsável pelos serviços de persistência.

 

Picture6-3

 

10 – Na pasta Serviços precisamos criar a classe concreta que irá implementar a nossa interface. Chamaremos esta classe de “CustomerService.cs

 

Primeira parte

 

Picture7-3

 

Segunda parte

 

Picture8-3

 

11 – Vamos agora registar a nossa injeção de dependência e nosso contexto na classe “Program.cs”.

 

Obs.: Já vamos aproveitar e registar o método de extensão “AddMemoryCache()” na linha 14, que irá permitir implementar a interface de uso do cache.

 

Picture9-2

 

 

12 – Estamos quase lá. Dentro da pasta Model vamos criar uma classe que servirá de transporte do dado quando criamos um Customer. Vamos chamar de “CustomerViewModel.cs”.

 

Picture10-2

 

13 – Finalmente vamos criar a nossa controller, que receberá as requisições, e inicialmente vamos implementar ela sem o uso de cache. Primeiramente, injetamos no construtor da controller o nosso serviço.

 

Picture11-2

 

14 – Na sequência criamos nossa primeira rota, responsável por criar uma lista de customers, bastando para isso informar a quantidade desejada. Isso irá adicionar na nossa base de dados os customers que vamos tratar para as outras operações.

 

Picture12-2

 

15 – Criamos mais dois enpoints. O primeiro podemos obter um customer por Id e o segundo podemos obter a listagem completa de customers na base de dados.

 

Picture13-2

 

16 – Os dois últimos enpoints são responsáveis por criar e remover customers.

 

Picture14-1

 

17 – Agora podemos correr nossa aplicação. Se tudo der certo, você deve ver a seguinte página.

 

Picture15-1

 

18 – Vamos agora consumir cada endpoint para ver os resultados iniciais, começando por criar 500 novos customers.

 

Picture16

 

19 – Ao listar os customers gerados vamos observar este resultado.

 

Picture17-1

 

20 – Agora vamos copiar um destes Id’s para obter apenas um customer. No meu caso, utilizei o Id do Customer_3.

 

Picture18-1

 

21 – Vamos agora criar um customer e buscar ele novamente por Id para ter certeza que foi criado.

 

Picture19-1

Picture20-1

 

22 – Por fim vamos remover um customer pelo seu Id.

 

Picture21-1

 

Bem, chegamos até aqui criando uma API totalmente funcional com praticamente todas as operações. Vamos agora implementar o uso de cache e observar o aumento significativo de performance. Também vamos remover os dados da memória logo um customer for criado ou removido, até mesmo antes do seu tempo de expiração.

 

1 – Injetamos a interface IMemoryCache no construtor do serviço “CustomerService.cs”.

 

Picture22

 

2 – O método que retorna todos os customers deve ser alterado para validar se a lista já está em memória. Caso já exista uma lista em memória, imediatamente será retornada para a controller e não vai obter o dado a partir da base de dados. Isso deverá ocorrer até que o tempo expire ou que haja alguma operação de exclusão/inclusão.

 

Picture23

 

Observação importante: o “MemoryCacheEntryOptions” nos permite configurar opções importantes. No nosso caso eu informei na linha 46 que o tempo de expiração absoluto é de 6 horas, porém se o serviço não for acedido por 10 minutos, o mesmo será excluído da memória. Existem outras opções como “SetPriority”, “SetSize” etc… você pode obter maiores informações na documentação da Microsoft (compartilharei link ao final do artigo).

 

3 – Agora uma parte muito importante da nossa implementação. A remoção ou inclusão de um novo customer deverá excluir o dado da memória, garantido assim que a próxima request possua a lista atualizada.

 

Em AddCustomer

 

Picture24

 

Em RemoveCustomers

 

Picture25

 

DESEMPENHO DA API

Com a nossa implementação praticamente finalizada, podemos falar sobre a melhoria na performance da API. Vamos alterar nossa controller para retornar além dos dados, o tempo de resposta utilizando cache.

 

Perceba que a primeira chamada do endpoint “GetCustomers()” será mais demorada, pois ela está indo até nossa base de dados e incluindo os dados em memória.

 

Picture26

 

Inicialmente criaremos 1000 registros.

 

Picture27

 

Agora vamos obter todos os Customers. Observe que a resposta dos 1000 registros ocorreu em 157 milissegundos.

 

Picture28

 

Vamos consumir o mesmo serviço novamente. Incrivelmente podemos verificar que o retorno foi em menos de 1 milissegundo - um ganho absurdo de desempenho. Imaginem a eficiência em seus projetos ao implementar cache.

 

Picture29

 

CONCLUSÃO

Memory Cache é uma ótima alternativa para quem busca performance, melhorar experiência para o cliente, reduzir carga na base de dados bem como economizar recursos em nuvem.

Saliento que não é uma solução para qualquer cenário. É preciso cautela no uso, mas desde que bem pensada e implementada vai ajudar a criar uma aplicação muito melhor.

 

MAIS INFORMAÇÃO:

 

FONTES:

 

Partilha este artigo