Redis .NET C# Performance Backend

Cache com Redis no .NET na prática: StackExchange.Redis, IDistributedCache e padrões

Guia completo de cache com Redis no .NET: como usar IDistributedCache e StackExchange.Redis, padrões cache-aside, cache de sessão.

N
Neryx Digital Architects
25 de setembro de 2025
12 min de leitura
230 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

Redis é a solução de cache mais usada em aplicações .NET de alta performance. Ele elimina consultas desnecessárias ao banco, acelera respostas de APIs e permite compartilhar estado entre múltiplas instâncias da aplicação. Este guia cobre do básico ao avançado — com código real e os padrões que funcionam em produção.

Setup: duas formas de usar Redis no .NET

O .NET oferece duas abstrações para Redis, com casos de uso distintos:

// Instalar os pacotes necessários:
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis   // IDistributedCache
dotnet add package StackExchange.Redis                               // acesso direto (mais poderoso)

// No Program.cs — registrar ambos: builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration[“Redis:ConnectionString”]; // Ex: “localhost:6379” ou “redis.myapp.com:6379,password=secret,ssl=true” options.InstanceName = “NeryxApp:”; // prefixo automático em todas as chaves });

// Para acesso direto via StackExchange.Redis: builder.Services.AddSingleton<IConnectionMultiplexer>(sp => ConnectionMultiplexer.Connect(builder.Configuration[“Redis:ConnectionString”]!));

IDistributedCache: cache simples e portável

A interface IDistributedCache é a abstração padrão do .NET. Funciona com Redis, Memcached ou cache em memória — você troca a implementação sem mudar o código da aplicação:

public class ProdutoService
{
    private readonly IDistributedCache _cache;
    private readonly AppDbContext _db;
public ProdutoService(IDistributedCache cache, AppDbContext db)
{
    _cache = cache;
    _db = db;
}

public async Task&lt;ProdutoDto?&gt; GetProdutoAsync(Guid id, CancellationToken ct)
{
    var cacheKey = $"produto:{id}";

    // 1. Tenta o cache primeiro
    var cached = await _cache.GetStringAsync(cacheKey, ct);
    if (cached != null)
        return JsonSerializer.Deserialize&lt;ProdutoDto&gt;(cached);

    // 2. Cache miss: busca no banco
    var produto = await _db.Produtos
        .AsNoTracking()
        .Where(p => p.Id == id)
        .Select(p => new ProdutoDto(p.Id, p.Nome, p.Preco, p.Categoria))
        .FirstOrDefaultAsync(ct);

    if (produto == null) return null;

    // 3. Armazena no cache com TTL de 10 minutos
    var options = new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
        SlidingExpiration = TimeSpan.FromMinutes(2) // renova TTL se acessado
    };

    await _cache.SetStringAsync(
        cacheKey,
        JsonSerializer.Serialize(produto),
        options,
        ct);

    return produto;
}

public async Task InvalidarCacheProdutoAsync(Guid id, CancellationToken ct)
{
    await _cache.RemoveAsync($"produto:{id}", ct);
}

}

Cache-Aside com extension method genérico

O padrão cache-aside é tão comum que vale encapsular numa extension method reutilizável:

public static class DistributedCacheExtensions
{
    public static async Task<T?> GetOrSetAsync<T>(
        this IDistributedCache cache,
        string key,
        Func<Task<T?>> factory,
        TimeSpan? absoluteExpiration = null,
        CancellationToken ct = default) where T : class
    {
        var cached = await cache.GetStringAsync(key, ct);
        if (cached != null)
            return JsonSerializer.Deserialize<T>(cached);
    var value = await factory();
    if (value == null) return null;

    var options = new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = absoluteExpiration ?? TimeSpan.FromMinutes(5)
    };

    await cache.SetStringAsync(key, JsonSerializer.Serialize(value), options, ct);
    return value;
}

}

// Uso — 3 linhas em vez de 20: public async Task<CategoriaDto?> GetCategoriaAsync(int id, CancellationToken ct) { return await _cache.GetOrSetAsync( key: $“categoria:{id}”, factory: () => _db.Categorias .Where(c => c.Id == id) .Select(c => new CategoriaDto(c.Id, c.Nome)) .FirstOrDefaultAsync(ct), absoluteExpiration: TimeSpan.FromHours(1), ct: ct); }

StackExchange.Redis: quando você precisa de mais poder

Para operações avançadas — counters atômicos, pub/sub, transações, Lua scripts — use IDatabase diretamente:

public class RateLimiterService
{
    private readonly IDatabase _redis;
public RateLimiterService(IConnectionMultiplexer mux)
    => _redis = mux.GetDatabase();

// Rate limiting com sliding window usando INCR + EXPIRE
public async Task&lt;bool&gt; IsAllowedAsync(string clientId, int maxRequests, TimeSpan window)
{
    var key = $"rate_limit:{clientId}:{DateTime.UtcNow:yyyyMMddHHmm}";

    // Incremento atômico — thread-safe sem locks
    var count = await _redis.StringIncrementAsync(key);

    if (count == 1)
        await _redis.KeyExpireAsync(key, window); // define TTL na primeira chamada

    return count &lt;= maxRequests;
}

}

public class ContadorService { private readonly IDatabase _redis;

public ContadorService(IConnectionMultiplexer mux) => _redis = mux.GetDatabase();

// Contador de views de produto (atômico, sem race condition)
public async Task IncrementarViewAsync(Guid produtoId)
    => await _redis.StringIncrementAsync($"produto:views:{produtoId}");

public async Task&lt;long&gt; GetViewsAsync(Guid produtoId)
    => (long)await _redis.StringGetAsync($"produto:views:{produtoId}");

// Hash para múltiplos campos num mesmo objeto Redis
public async Task SalvarSessaoAsync(string sessionId, Dictionary&lt;string, string&gt; dados)
{
    var entries = dados.Select(kv => new HashEntry(kv.Key, kv.Value)).ToArray();
    await _redis.HashSetAsync($"session:{sessionId}", entries);
    await _redis.KeyExpireAsync($"session:{sessionId}", TimeSpan.FromHours(24));
}

public async Task&lt;Dictionary&lt;string, string&gt;&gt; GetSessaoAsync(string sessionId)
{
    var entries = await _redis.HashGetAllAsync($"session:{sessionId}");
    return entries.ToDictionary(e => e.Name.ToString(), e => e.Value.ToString());
}

}

Cache Stampede: o problema que derruba produção

Quando uma chave popular expira, centenas de requests simultâneos podem ir ao banco ao mesmo tempo — o "cache stampede". A solução é o padrão probabilistic early expiration ou um lock distribuído:

public class CacheComLockService
{
    private readonly IDatabase _redis;
    private readonly AppDbContext _db;
// Solução: lock distribuído para evitar cache stampede
public async Task&lt;RelatorioDto?&gt; GetRelatorioAsync(string tipo, CancellationToken ct)
{
    var cacheKey = $"relatorio:{tipo}";
    var lockKey = $"lock:relatorio:{tipo}";

    // 1. Tenta o cache (sem lock — leitura é rápida)
    var cached = await _redis.StringGetAsync(cacheKey);
    if (cached.HasValue)
        return JsonSerializer.Deserialize&lt;RelatorioDto&gt;(cached!);

    // 2. Cache miss: tenta adquirir lock
    var lockAcquired = await _redis.StringSetAsync(
        lockKey, "1",
        TimeSpan.FromSeconds(30),
        When.NotExists); // SET NX — atômico, só um processo ganha

    if (!lockAcquired)
    {
        // Outro processo está regenerando. Aguarda e tenta de novo.
        await Task.Delay(200, ct);
        var retryCache = await _redis.StringGetAsync(cacheKey);
        return retryCache.HasValue
            ? JsonSerializer.Deserialize&lt;RelatorioDto&gt;(retryCache!)
            : null;
    }

    try
    {
        // 3. Somente o processo com lock vai ao banco
        var relatorio = await GerarRelatorioAsync(tipo, ct);
        await _redis.StringSetAsync(
            cacheKey,
            JsonSerializer.Serialize(relatorio),
            TimeSpan.FromMinutes(15));
        return relatorio;
    }
    finally
    {
        await _redis.KeyDeleteAsync(lockKey);
    }
}

private async Task&lt;RelatorioDto&gt; GerarRelatorioAsync(string tipo, CancellationToken ct)
{
    // query pesada ao banco...
    await Task.Delay(100, ct); // simulação
    return new RelatorioDto(tipo, DateTime.UtcNow, new List&lt;ItemRelatorio&gt;());
}

}

Invalidação de cache em cascata

Quando um produto é atualizado, todas as chaves relacionadas precisam ser invalidadas. Use prefixos de chave com Lua script ou o padrão pub/sub:

public class InvalidacaoCacheService
{
    private readonly IDatabase _redis;
    private readonly IServer _server;
public InvalidacaoCacheService(IConnectionMultiplexer mux)
{
    _redis = mux.GetDatabase();
    _server = mux.GetServer(mux.GetEndPoints()[0]);
}

// Invalida todas as chaves de um produto (produto detalhe + categorias + listas)
public async Task InvalidarProdutoAsync(Guid produtoId)
{
    // Padrão: busca todas as chaves com o prefixo (use com cuidado em Redis grande)
    var pattern = $"NeryxApp:produto:{produtoId}*";
    var keys = _server.Keys(pattern: pattern).ToArray();

    if (keys.Any())
        await _redis.KeyDeleteAsync(keys);

    // Publica evento para outros serviços invalidarem caches locais
    await _redis.PublishAsync("cache.invalidado", $"produto:{produtoId}");
}

// Subscriber em outro serviço:
public void SubscribeInvalidacao(ISubscriber sub)
{
    sub.Subscribe("cache.invalidado", (channel, message) =>
    {
        // Remove do cache local (IMemoryCache) se existir
        Console.WriteLine($"Cache invalidado: {message}");
    });
}

}

Configuração de produção: connection pooling e resiliência

// ConnectionMultiplexer com configurações de produção
var configOptions = new ConfigurationOptions
{
    EndPoints = { "redis-primary:6379", "redis-replica:6379" },
    Password = "senha-secreta",
    Ssl = true,
    AbortOnConnectFail = false,     // não falha na inicialização se Redis estiver down
    ConnectRetry = 3,               // tentativas de reconexão
    ConnectTimeout = 5000,          // timeout de conexão (ms)
    SyncTimeout = 5000,             // timeout de operação síncrona (ms)
    ReconnectRetryPolicy = new ExponentialRetry(5000), // backoff exponencial
    DefaultDatabase = 0,
};

var mux = await ConnectionMultiplexer.ConnectAsync(configOptions);

// Em produção com AWS ElastiCache ou Azure Cache for Redis: // “my-redis.cache.windows.net:6380,password=…,ssl=True,abortConnect=False”

Estratégias de TTL: quanto tempo cachear?

Não existe TTL universal. A regra prática é baseada na frequência de atualização e custo de dado desatualizado:

  • Dados estáticos (categorias, configurações): 1–24 horas com invalidação on-write
  • Dados semi-dinâmicos (produtos, preços): 5–30 minutos com invalidação on-write
  • Dados por usuário (carrinho, sessão): sliding expiration de 30 min a 24h
  • Resultados de queries pesadas (relatórios, dashboards): 1–15 minutos sem invalidação
  • Rate limiting / counters: TTL = tamanho da janela de tempo

Checklist de cache Redis em produção

  • Use prefixo de instância (InstanceName) para separar ambientes no mesmo Redis
  • Sempre defina TTL — chaves sem expiração lotam a memória
  • Implemente circuit breaker: se o Redis cair, a aplicação deve continuar (busca no banco)
  • Monitore hit rate — abaixo de 80% indica chaves curtas demais ou padrão de acesso ruim
  • maxmemory-policy allkeys-lru no Redis.conf para produção (evicção automática quando cheio)
  • Não cache dados sensíveis (senhas, tokens) sem criptografia adicional

Conclusão

Redis no .NET é simples de começar com IDistributedCache e extremamente poderoso quando você acessa IDatabase diretamente para operações atômicas. O padrão cache-aside cobre 90% dos casos, e o lock distribuído resolve cache stampede. O segredo está em definir TTLs corretos e implementar invalidação on-write para dados críticos.

Se você está enfrentando gargalos de banco de dados em APIs de alto volume, ou precisa implementar cache distribuído em um sistema com múltiplas instâncias, a Neryx pode ajudar com análise e implementação. Consultoria inicial gratuita.

Leitura complementar:

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.