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<ProdutoDto?> 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<ProdutoDto>(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<bool> 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 <= 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<long> 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<string, string> 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<Dictionary<string, string>> 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<RelatorioDto?> 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<RelatorioDto>(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<RelatorioDto>(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<RelatorioDto> GerarRelatorioAsync(string tipo, CancellationToken ct) { // query pesada ao banco... await Task.Delay(100, ct); // simulação return new RelatorioDto(tipo, DateTime.UtcNow, new List<ItemRelatorio>()); }
}
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-lruno 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: