Implementando cache distribuído no ASP.NET Core

No artigo passado vimos como implementar cache de memória em uma aplicação ASP.NET Core. Entretanto, este tipo de cache não fornece bons resultados quando há mais de uma instância da aplicação, por exemplo, quando é publicada em PaaS. Para este cenário pode ser utilizado cache distribuído.

Cache distribuído

Cache distribuído é um cache compartilhado entre as instâncias de uma aplicação. Tipicamente gerenciado por um serviço externo que a aplicação acessa. Este tipo de cache pode melhorar a performance da aplicação, além de facilitar a sua escalabilidade.

Quando se trabalha com cache distribuído, o dado:

  • É coerente (consistente) entre as requisições pelos vários servidores da aplicação;
  • Não se perde se a aplicação reiniciar;
  • Não utiliza a memória local.

No ASP.NET Core o cache distribuído pode ser implementado, utilizando SQL Server, Redis ou serviços de terceiro, como o NCache. Como o NCache é uma biblioteca paga, não irei abordá-la neste artigo. No caso das outras duas formas, começaremos nosso estudo pelo Redis.

O que é o Redis?

O Redis é um banco de dados estruturados em memória, open source, que pode ser utilizado como database, cache e/ou mensageria. O seu uso mais comum é como sistema de cache e é como o utilizaremos neste artigo.

Para que seja utilizado é necessário que o Redis esteja instalado na máquina atual ou em algum servidor. O seu instalador pode ser obtido no site dele. Entretanto, neste artigo farei uso a versão “containeralizada”, via Docker.

Com isso, caso tenha o Docker instalado na sua máquina, o Redis pode ser inicializado com o comando abaixo:

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

Como Docker não é o foco deste artigo, não expliquei o comando acima, nele o importante é a porta definida no host local (6379), pois ela será informada no ASP.NET Core.

Implementando cache distribuído no ASP.NET Core

Assim como o cache de memória, o cache distribuído é implementado via uma interface, a IDistributedCache. Entretanto o ASP.NET não fornece uma implementação padrão para esta interface, cada uma das implementações de cache distribuído são fornecidas via pacotes NuGet.

No caso do Redis, é necessário adicionar na aplicação o pacote Microsoft.Extensions.Caching.StackExchangeRedis:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Em seguida é necessário registrá-lo no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    //...

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

Com isso, a implementação da interface IDistributedCache será “injetada” das dependências da aplicação e poderá ser acessada no construtor:

public class ProductsController : Controller
{
    private readonly IDistributedCache _cache;

    public ProductsController(IDistributedCache cache)
    {
        _cache = cache;
    }
}
C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Para ter acesso a um valor do cache podemos utilizar os métodos Get, GetAsync, que retorna o valor como um array de bytes; ou GetString, GetStringAsync, que o retorna como uma string:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";
    var products = new List<Product>();

    var json = await _cache.GetStringAsync(cacheKey);
    if(json != null)
    {
        products = JsonSerializer.Deserialize<List<Product>>(json);
    }

    return View(products);
}

Já para salvar um valor no cache, também temos quatro métodos: Set e SetAsync, que salva o valor como um array de bytes; e SetString e SetStringAsync que o salva como uma string:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";
    var products = new List<Product>();

    var json = await _cache.GetStringAsync(cacheKey);
    if(json != null)
    {
        products = JsonSerializer.Deserialize<List<Product>>(json);
    }
    else {
        products = await _context.Products.ToListAsync();
        json = JsonSerializer.Serialize<List<Product>>(products);
        await _cache.SetStringAsync(cacheKey, json);
    }

    return View(products);
}

Caso queira, também é possível remover o item do cache com os métodos Remove e RemoveAsync.

Definindo uma expiração para o cache

Pela nossa configuração atual, podemos correr o risco de ter dados obsoletos no cache, já que não está sendo definido nenhum prazo de expiração dos dados salvos nele. Podemos fazer isso utilizando a classe DistributedCacheEntryOptions:

products = await _context.Products.ToListAsync();
json = JsonSerializer.Serialize<List<Product>>(products);

var options = new DistributedCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromSeconds(20))
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(1));

await _cache.SetStringAsync(cacheKey, json);

Agora o cache poderá ficar inativo até 20 segundos e será removido depois de um minuto. O tempo do “SlidingExpiration” também pode ser reiniciado com os métodos Refresh e RefreshAsync.

Implementando cache distribuído com SQL Server

Caso tenha intenção de utilizar o SQL Server no lugar do Redis como cache distribuído, inicialmente é necessário executar o comando abaixo:

dotnet sql-cache create <conexao-sql-server> <schema> <tabela>

Para criar no banco a tabela que será utilizada para armazenar os dados do cache.

Em seguida, adicione no projeto o pacote Microsoft.Extensions.Caching.SqlServer:

dotnet add package Microsoft.Extensions.Caching.SqlServer

E por fim, configure o cache no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{
        //....
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = "<conexao-sql-server>";
        options.SchemaName = <schema>;
        options.TableName = <tabela>;
    });
}

Note que nesta configuração devem ser informados os mesmos dados passados no comando dotnet sql-cache create.

O uso deste tipo de cache não difere do que vimos com o Redis.

Conclusão

Como o cache de memória deve ser evitado em uma aplicação “multi-servidor”, se este for o seu caso, não hesite em implementar o cache distribuído. Se a sua equipe estiver mais familiarizada com o SQL Server, mesmo que a sua performance seja inferior ao Redis, esta é uma boa escolha. Se este não for o caso, procure sempre optar por uma solução “em-memória”, como o Redis e o NCache.

Deixe seu comentário

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200