ASP .NET Core -  Usando cache distribuído com Redis


Hoje veremos o que é e como usar o cache distribuído usando o Redis.

Eu já tratei da utilização do cache em aplicações ASP .NET Core em outros artigos (veja as referências) e hoje vamos focar na utilização do cache distribuído em aplicações ASP.NET Core.

O cache distribuído é um cache externo à aplicação que é compartilhado entre as instâncias de uma aplicação normalmente mantido como um serviço externo para os servidores de aplicativos que o acessam. 

A ASP .NET Core suporta tanto o cache em memória como o cache distribuído e este recurso pode melhorar o desempenho e a escalabilidade das aplicações ASP .NET Core sendo esses os principais motivos de usá-lo.

As vantagens do cache distribuído são:

No entanto, a implementação do Cache distribuído é específica do aplicativo, e, isso significa que existem vários provedores de cache que oferecem suporte a caches distribuídos. Na ASP .NET Core podemos implementar o cache distribuído usando o SQL Server, o Redis, o NCache, etc.

Como neste artigo vamos focar na utilização do Redis, vamos falar mais pouco sobre ele.(novamente)

Apresentando o Redis

O Redis é um armazenamento de dados de código aberto que pode ser usado como banco de dados, cache ou intermediário de mensagens. Ele suporta muitas estruturas de dados como string, hashes, listas, consultas e tipos complexos. É um banco de dados No SQL baseado no armazenamento de dados no formato valor-chave extremamente rápido que é escrito em C. Não é a toa que ele é usado por empresas como Stackoverflow, Flickr, Github.

Em nosso contexto, o Redis é uma ótima opção para implementar um cache de alta disponibilidade para reduzir a latência de acesso aos dados e melhorar o tempo de resposta do aplicativo. Como resultado, podemos reduzir bastante a carga de nosso banco de dados.

O Redis foi desenvolvido para ser executado em sistemas operacionais Linux, OSX e BSD. Oficialmente, ele não tem suporte para sistema operacional baseado em Windows. Mas há várias portes disponíveis para o Windows 10. Em geral é usada uma máquina secundária que cuida do cache, onde o Redis é executado e pode servir como uma memória cache para vários aplicativos na nuvem.

Instalando o Redis no Windows   

Para fazer a instalação do Redis no Windows podemos baixar o pacote ZIP a partir deste repositório : GitHub Repo   

Extraia a pasta Zip e abra o arquivo redis-server.exe. Pronto,  o servidor Redis agora está sendo executado em sua máquina. Não feche esta janela.

Essa seria a opção de instalar o Redis na sua máquina local.

Criando um contêiner Docker

Outra opção é usar um contêiner Docker com uma imagem do Redis. Para isso você precisa ter o Docker instalado.

Estou usando o Docker for Desktop for Windows e para levantar um contêiner com o Redis podemos abrir uma janela de comandos via PowerShell e digitar:

docker run --name local-redis -p 6379:6379 -d redis

Para conferir o contêiner em execução digite o comando :  docker container ps

Vamos agora entrar no contêiner e emitir alguns comandos para testar o Redis. Para isso digite o seguinte comando:

docker exec -it local-redis sh

Você verá o sinal # indicando que entrou no contêiner.

Digite redis-cli que é a interface de linha de comando do Redis a partir de onde podemos enviar comandos ao Redis e ler as respostas enviadas pelo servidor, diretamente do terminal.

A seguir digite ping e terá a resposta PONG.

Vamos testar alguns comandos como atribuir uma chave usando o comando set , obter o valor e a seguir incrementar um contador:

Para sair do cliente digite exit e para sair do contêiner digite novamente exit.

Com isso vemos que o Redis esta funcionando corretamente no contêiner Docker criado.

Neste exemplo eu vou usar o contêiner Docker e assim não vou instalar o Redis localmente.

Acessando e usando um cache distribuído com IDistributedCache

Para poder acessar os objetos na implementação de um cache distribuído podemos usar a interface IDistributedCache que fornece os seguintes métodos :

Assim a interface IDistributedCache representa um cache distribuído de valores serializados e deverá ser usada para acessar e trabalhar com o cache distribuído que for implementado.

Entretanto a ASP.NET Core não fornece uma implementação padrão para esta interface, e assim cada uma das implementações de cache distribuído são fornecidas via pacotes NuGet.

Integrando o Redis com a ASP .NET Core

Para mostrar a utilização do cache distribuído usando o Redis vamos criar uma aplicação ASP .NET Core Web API que vai acessar a tabela Orders do banco de dados Northwind usando o EF Core e retornar a lista de todos os pedidos. Teremos assim algo e torno de 1000 registros e poderemos comparar o desempenho usando o cache.

Como propósito do artigo é mostrar como usar o cache distribuído eu não vou mostrar detalhes de implementação da API que foi criada.

Para poder acessar e usar o cache distribuído usando o Redis teremos que incluir no projeto o seguinte pacote :

Install-Package Microsoft.Extensions.Caching.StackExchangeRedis

Depois disso, temos que configurar nosso aplicativo para suportar o cache do Redis e especificar a porta em que o Redis está disponível.

Para fazer isso, abra o arquivo Startup.cs e no método ConfigureServices e adicione o seguinte código:

services.AddStackExchangeRedisCache(options =>
{
     options.Configuration = "localhost:6379";
});

A seguir temos o código do controlador OrdersController criado na pasta Controllers:

using Aspn_Redis1.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Aspn_Redis1.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly IDistributedCache distributedCache;
        private readonly NorthwindContext context;
        public OrdersController(NorthwindContext context, IDistributedCache distributedCache)
        {
            this.context = context;
            this.distributedCache = distributedCache;
        }
        [HttpGet("normal")]
        public async Task<IActionResult> GetAllOrders()
        {
                var orderList = await context.Orders.ToListAsync();
                return Ok(orderList);
        }
        [HttpGet("redis")]
        public async Task<IActionResult> GetAllOrdersUsingRedisCache()
        {
            var cacheKey = "orderList";
            string serializedOrderList;
            var orderList = new List<Order>();
            var redisOrderList = await distributedCache.GetAsync(cacheKey);
            if (redisOrderList != null)
            {
                serializedOrderList = Encoding.UTF8.GetString(redisOrderList);
                orderList = JsonConvert.DeserializeObject<List<Order>>(serializedOrderList);
            }
            else
            {
                orderList = await context.Orders.ToListAsync();
                serializedOrderList = JsonConvert.SerializeObject(orderList);
                redisOrderList = Encoding.UTF8.GetBytes(serializedOrderList);
                var options = new DistributedCacheEntryOptions()
                      .SetAbsoluteExpiration(DateTime.Now.AddMinutes(10))
                      .SetSlidingExpiration(TimeSpan.FromMinutes(2));
                await distributedCache.SetAsync(cacheKey, redisOrderList, options);
            }
            return Ok(orderList);
        }
    }
}

No código da API definimos dois endpoints :

Injetamos no construtor do controlador o contexto do EF Core e a interface IDistributedCache :

  private readonly IDistributedCache distributedCache;
  private readonly NorthwindContext context;
  public OrdersController(NorthwindContext context, IDistributedCache distributedCache)
  {
       this.context = context;
       this.distributedCache = distributedCache;
  }

Para ter acesso a um valor do cache usamos o método GetAsync() que retorna o valor como um array de bytes;

    var redisOrderList = await distributedCache.GetAsync(cacheKey);

E precisamos desserializar o array de bytes obtidos se ele existir no cache:

  if (redisOrderList != null)
  {
        serializedOrderList = Encoding.UTF8.GetString(redisOrderList);
         orderList = JsonConvert.DeserializeObject<List<Order>>(serializedOrderList);
   }

Caso contrário obtemos os dados e usando o método SetAsync() incluímos os dados no cache:

else
{
   orderList = await context.Orders.ToListAsync();

   serializedOrderList = JsonConvert.SerializeObject(orderList);

   redisOrderList = Encoding.UTF8.GetBytes(serializedOrderList);

    var options = new DistributedCacheEntryOptions()
            .SetAbsoluteExpiration(DateTime.Now.AddMinutes(10))
            .SetSlidingExpiration(TimeSpan.FromMinutes(2));

await distributedCache.SetAsync(cacheKey, redisOrderList, options);
}

Para não correr o risco de ter dados desatualizados no cache vamos definir um prazo de expiração para o cache usando a classe  DistributedCacheEntryOptions atribuindo valores aos métodos de extensão SetAbsoluteExpiration e SetSlidingExpiration.

Assim no primeiro acesso o tempo gasto para obter os dados deverá ser maior e a seguir os dados estarão no cache e o retorno deverá ser mais rápido.

Testando a implementação

Estamos usando o Swagger em nosso projeto ASP .NET Core e ao executar o projeto teremos a exibição dos dois endpoints criados na API conforme imagem a seguir:

No entanto vamos usar o Postman para testar o acesso aos endpoints.

Para instalar o Postman acesse esse link : https://www.getpostman.com/apps ou abra o Google Chrome e digite postam e a seguir clique no link:  Postman - Chrome Web Store

A seguir abrindo o Postman vamos realizar o primeiro acesso: https://localhost:44349/api/Orders/redis

Abaixo temos o resultado obtido e tempo gasto que foi algo em torno de 1328 ms:

A seguir vamos fazer a segunda chamada e agora o cache já esta definido no Redis pela chave orderList. Assim, a segunda chamada será direta para o cache do Redis, o que deveria reduzir significativamente o tempo de resposta.

Como podemos confirmar na figura abaixo o tempo gasto foi 60 ms, ou seja, menos da metade do primeiro acesso:

A percepção do tempo gasto será maior se a quantidade de dados for maior, mas já vimos que usar o cache distribuído reduz bastante o desempenho de acesso aos dados.

Em outro artigo vamos mostrar como fazer a mesma coisa usando o SQL Server.

Pegue o projeto aqui:  Aspn_Redis1.zip (sem as referências)

"Aquele que diz: "Eu o conheço (Jesus)", mas não guarda os seus mandamentos, esse é mentiroso, e a verdade não está nele."
1 João 2:4

Referências:


José Carlos Macoratti