This content originally appeared on DEV Community and was authored by Bruno Silva
Fala, pessoal!
Hoje falar um papo sobre cache multi-nível e como implementar isso de forma prática usando o FusionCache.
A ideia é mostrar como deixar sua aplicação mais rápida, resiliente e preparada para lidar com cenários de alta demanda. Bora lá?
Antes de ir para o código, precisamos entender os conceitos.
O que é Cache Multi-Nível?
Cache multi-nível é uma arquitetura que combina diferentes tipos de cache para otimizar performance:
- L1 (Level 1): Cache em memória local – ultra-rápido (~0.1ms)
- L2 (Level 2): Cache distribuído (Redis/Valkey) – rápido e compartilhado (~1-5ms)
Por exemplo, em APIs podemos usar cache L1 para dados acessados frequentemente e cache L2 para compartilhar dados entre múltiplas instâncias da aplicação.
Por que FusionCache?
O FusionCache é uma biblioteca .NET que oferece:
Cache Multi-Nível (L1 + L2 automático)
Backplane (sincronização entre instâncias)
Fail-Safe (funciona mesmo se Redis cair)
Cache Stampede Protection (evita sobrecarga)
Timeouts Configuráveis (performance otimizada)
Cenário de Exemplo
Vamos imaginar uma API que precisa trabalhar com cache, mas diferenciando entre dois tipos de dados: críticos e simples.
Mas por que separar assim?
A ideia é ter mais controle sobre o tempo de vida de cada cache de acordo com a importância da informação e isso era uma necessidade real que eu tinha.
Crítico: dados muito requisitados (coisa de 1 milhão de requisições por dia) e que mudam pouco. Aqui vale a pena usar L1 + L2, garantindo performance máxima e resiliência.
Simples: dados menos importantes, que não precisam ficar tanto tempo em memória. Nesse caso podemos buscar mais vezes no Redis/Valkey, usando principalmente o L2.
Implementação em .NET 8
1. Instalação dos Pacotes NuGet
dotnet add package ZiggyCreatures.FusionCache
dotnet add package ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson
dotnet add package ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis
2. Configuração no Program.cs
// Configuração do Redis/Valkey
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var connectionString = builder.Configuration.GetConnectionString("Redis") ?? "localhost:6379";
var configurationOptions = ConfigurationOptions.Parse(connectionString);
configurationOptions.AbortOnConnectFail = false;
configurationOptions.ConnectRetry = 1;
configurationOptions.ConnectTimeout = 1000;
configurationOptions.SyncTimeout = 3000;
return ConnectionMultiplexer.Connect(configurationOptions);
});
// Cache distribuído (L2)
builder.Services.AddSingleton<IDistributedCache>(sp =>
{
var redis = sp.GetRequiredService<IConnectionMultiplexer>();
return new RedisCache(new RedisCacheOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(redis),
InstanceName = ""
});
});
// FusionCache Crítico (L1 + L2 + Backplane) - Para produtos
builder.Services.AddFusionCache("CacheCritico")
.WithDefaultEntryOptions(new FusionCacheEntryOptions
{
Duration = TimeSpan.FromMinutes(10), // L1: 10 minutos
DistributedCacheDuration = TimeSpan.FromMinutes(30), // L2: 30 minutos
FactorySoftTimeout = TimeSpan.FromMilliseconds(100),
FactoryHardTimeout = TimeSpan.FromSeconds(2),
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(2),
FailSafeThrottleDuration = TimeSpan.FromSeconds(30)
})
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
.WithDistributedCache(sp => sp.GetRequiredService<IDistributedCache>())
.WithBackplane(new RedisBackplane(new RedisBackplaneOptions
{
Configuration = builder.Configuration.GetConnectionString("Redis")
}));
// FusionCache Simples (microcaching + L2) - Para relatórios
builder.Services.AddFusionCache("CacheSimples")
.WithDefaultEntryOptions(new FusionCacheEntryOptions
{
Duration = TimeSpan.FromSeconds(30), // L1: 30 segundos (microcaching)
DistributedCacheDuration = TimeSpan.FromMinutes(60), // L2: 60 minutos
FactorySoftTimeout = TimeSpan.FromMilliseconds(200),
FactoryHardTimeout = TimeSpan.FromSeconds(5),
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(1),
FailSafeThrottleDuration = TimeSpan.FromSeconds(15)
})
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
.WithDistributedCache(sp => sp.GetRequiredService<IDistributedCache>())
.WithBackplane(new RedisBackplane(new RedisBackplaneOptions
{
Configuration = builder.Configuration.GetConnectionString("Redis")
}));
var app = builder.Build();
Aqui estamos registrando o FusionCache e dizendo que ele vai usar tanto memória local quanto Redis como backplane. Simples, né?
3. Interface de Cache Personalizada
public interface ICacheService
{
Task<T> BuscarAsync<T>(string chave);
Task CriarAsync<T>(string chave, T valor, TimeSpan? expiracao = null);
Task<T> BuscarOuCriarAsync<T>(string chave, Func<Task<T>> factory, TimeSpan? expiracao = null);
Task RemoverAsync(string chave);
Task<bool> ExisteAsync(string chave);
Task RemoverPorPrefixoAsync(string prefixo);
}
4. Implementação do Serviço de Cache
public class FusionCacheService : ICacheService
{
private readonly IFusionCache _cacheCritico;
private readonly IFusionCache _cacheSimples;
private readonly IConnectionMultiplexer _connectionMultiplexer;
private readonly IDatabase _database;
public FusionCacheService(
IFusionCacheProvider fusionCacheProvider,
IConnectionMultiplexer connectionMultiplexer)
{
_cacheCritico = fusionCacheProvider.GetCache("CacheCritico");
_cacheSimples = fusionCacheProvider.GetCache("CacheSimples");
_connectionMultiplexer = connectionMultiplexer;
_database = connectionMultiplexer?.GetDatabase(0);
}
public async Task<T> BuscarAsync<T>(string chave)
{
var cache = EscolherCache(chave);
return await cache.GetOrDefaultAsync<T>(chave);
}
public async Task CriarAsync<T>(string chave, T valor, TimeSpan? expiracao = null, TimeSpan? expiracaoMemoria = null)
{
var cache = EscolherCache(chave);
var options = new FusionCacheEntryOptions();
if (expiracao.HasValue)
{
options.DistributedCacheDuration = expiracao.Value;
}
if (expiracaoMemoria.HasValue)
{
options.Duration= expiracaoMemoria.Value;
}
await cache.SetAsync(chave, valor, options);
}
public async Task<T> BuscarOuCriarAsync<T>(string chave, Func<Task<T>> factory, TimeSpan? expiracao = null, TimeSpan? expiracaoMemoria = null)
{
var cache = EscolherCache(chave);
var options = new FusionCacheEntryOptions();
if (expiracao.HasValue)
{
options.DistributedCacheDuration = expiracao.Value;
}
if (expiracaoMemoria.HasValue)
{
options.Duration= expiracaoMemoria.Value;
}
return await cache.GetOrSetAsync<T>(chave, async (ctx, token) => await factory(), options);
}
public async Task RemoverAsync(string chave)
{
// Remove de ambos os caches (ativa Backplane automaticamente)
await _cacheCritico.RemoveAsync(chave);
await _cacheSimples.RemoveAsync(chave);
}
public async Task<bool> ExisteAsync(string chave)
{
var cache = EscolherCache(chave);
var valor = await cache.GetOrDefaultAsync<object>(chave);
return valor != null;
}
public async Task RemoverPorPrefixoAsync(string prefixo)
{
if (_connectionMultiplexer?.IsConnected == true)
{
var server = _connectionMultiplexer.GetServer(_connectionMultiplexer.GetEndPoints().First());
var pattern = $"*{prefixo}*";
// Busca chaves com SCAN e remove via FusionCache (ativa Backplane)
await foreach (var chaveRedis in server.KeysAsync(pattern: pattern))
{
var chaveLimpa = ExtrairChaveFusionCache(chaveRedis);
await _cacheCritico.RemoveAsync(chaveLimpa);
await _cacheSimples.RemoveAsync(chaveLimpa);
}
}
}
/// <summary>
/// Escolhe qual tipo de cache usar baseado na criticidade dos dados
/// </summary>
private IFusionCache EscolherCache(string chave)
{
// Dados críticos: usa cache completo (L1 + L2)
if (chave.StartsWith("produto:") || chave.StartsWith("usuario:"))
return _cacheCritico;
// Dados menos críticos: usa microcaching + L2
return _cacheSimples;
}
/// <summary>
/// Remove prefixo de versão do FusionCache (v0:, v1:, etc.)
/// </summary>
private string ExtrairChaveFusionCache(RedisKey chaveRedis)
{
var chaveStr = chaveRedis.ToString();
var indiceDoisPontos = chaveStr.IndexOf(':');
if (indiceDoisPontos > 0)
{
var possivelPrefixo = chaveStr.Substring(0, indiceDoisPontos + 1);
// Verifica se é prefixo de versão (v + números + :)
if (possivelPrefixo.StartsWith("v") && possivelPrefixo.EndsWith(":"))
{
var numeroVersao = possivelPrefixo.Substring(1, possivelPrefixo.Length - 2);
if (numeroVersao.All(char.IsDigit))
{
return chaveStr.Substring(possivelPrefixo.Length);
}
}
}
return chaveStr;
}
}
5. Configuração no appsettings.json
{
"ConnectionStrings": {
"Redis": "localhost:6379"
},
"Cache": {
"Tipo": "FusionCache"
}
}
6. Uso nos Controllers
Agora vamos ver como aplicar estratégias de cache diferentes nos nossos Controllers.
Obs: no exemplo vou mostrar direto na Controller só pra facilitar o entendimento.
Mas, seguindo boas práticas, o ideal é ter uma camada de Services responsável pelo acesso aos dados, deixando a Controller apenas como “ponte” entre requisição e regra de negócio.
No primeiro endpoint temos cache de produtos críticos com L1 + L2.
No segundo endpoint foi implementado cache de relatórios com microcaching.
No terceiro endpoint mostramos remoção por prefixo para invalidar categorias inteiras.
Código do Controller:
[ApiController]
[Route("api/[controller]")]
public class ProdutosController : ControllerBase
{
private readonly ICacheService _cache;
private readonly IProdutoRepository _produtoRepository;
public ProdutosController(ICacheService cache, IProdutoRepository produtoRepository)
{
_cache = cache;
_produtoRepository = produtoRepository;
}
[HttpGet("{id}")]
public async Task<IActionResult> BuscarProduto(int id)
{
// Cache crítico - L1 (10min) + L2 (20min)
var produto = await _cache.BuscarOuCriarAsync(
$"produto:{id}",
async () => await _produtoRepository.BuscarPorIdAsync(id),
TimeSpan.FromMinutes(20),
TimeSpan.FromMinutes(10)
);
return Ok(produto);
}
[HttpGet("categoria/{categoriaId}")]
public async Task<IActionResult> BuscarProdutosPorCategoria(int categoriaId)
{
// Cache simples - Microcaching (30s) + L2 (60min)
var produtos = await _cache.BuscarOuCriarAsync(
$"categoria:{categoriaId}",
async () => await _produtoRepository.BuscarPorCategoriaAsync(categoriaId)
);
return Ok(produtos);
}
[HttpPost("{id}")]
public async Task<IActionResult> AtualizarProduto(int id, [FromBody] Produto produto)
{
// Atualiza no banco
await _produtoRepository.AtualizarAsync(produto);
// Remove cache específico (ativa Backplane - sincroniza todas as instâncias)
await _cache.RemoverAsync($"produto:{id}");
// Remove caches relacionados por prefixo
await _cache.RemoverPorPrefixoAsync($"produtos:categoria:{produto.CategoriaId}");
return Ok();
}
[HttpGet("relatorio/vendas")]
public async Task<IActionResult> RelatorioVendas()
{
// Relatório - Cache simples com TTL longo
var relatorio = await _cache.BuscarOuCriarAsync(
"relatorio:vendas:diario",
async () => await GerarRelatorioVendas(),
TimeSpan.FromHours(1), TimeSpan.FromMinutes(30)
);
return Ok(relatorio);
}
private async Task<object> GerarRelatorioVendas()
{
// Simula processamento pesado
await Task.Delay(2000);
return new {
TotalVendas = 15000m,
Data = DateTime.Now,
TotalProdutos = 150
};
}
}
Principais Benefícios Obtidos
Performance Dramática
- Cache Hit L1: ~0.1ms (1000x mais rápido que banco)
- Cache Hit L2: ~2ms (50x mais rápido que banco)
- Cache Miss: Tempo normal do banco
Sincronização Entre Instâncias
// Problema antes do Backplane:
// Instância A: Remove cache às 10:30
// Instância B: Cache válido até 10:35 (5 min desatualizado!)
// Solução com Backplane:
// Instância A: Remove cache às 10:30
// Backplane: Notifica TODAS as instâncias instantaneamente
// Instância B: Remove cache automaticamente às 10:30
// ✅ Todas as instâncias sincronizadas!
Resiliência Total
// Cenário: Redis fica offline
// ✅ L1 (memória) continua funcionando
// ✅ Fail-Safe serve dados em cache mesmo expirados
// ✅ Aplicação NÃO para, NÃO trava
// ✅ Quando Redis volta, reconecta automaticamente
Configurações Avançadas
Cache por Tipo de Dados
private IFusionCache EscolherCache(string chave)
{
// Dados críticos: L1 + L2 completo
if (chave.StartsWith("produto:") ||
chave.StartsWith("usuario:") ||
chave.StartsWith("config:"))
return _cacheCritico;
// Dados menos críticos: microcaching + L2
return _cacheSimples;
}
TTL Diferenciado
// Cache crítico com TTL otimizado
await _cache.CriarAsync("produto:123", produto,
TimeSpan.FromMinutes(30), // L2: 30 minutos
TimeSpan.FromMinutes(5) // L1:5 minutos
);
// Cache de relatório com TTL longo
await _cache.CriarAsync("relatorio:vendas", relatorio,
TimeSpan.FromHours(4), // L2: 4 horas
TimeSpan.FromMinutes(1) // L1: 1 Minuto (microcaching)
);
Remoção Inteligente por Prefixo
public async Task AtualizarCategoria(int categoriaId)
{
// Atualiza dados no banco
await _repository.AtualizarCategoria(categoriaId);
// Remove TODOS os caches relacionados (em TODAS as instâncias)
await _cache.RemoverPorPrefixoAsync($"produtos:categoria:{categoriaId}");
// Próximas consultas já pegam dados atualizados!
}
Exemplo de Teste no Swagger
Teste 1: Cache Hit
GET /api/produtos/123
Response Time: 0.2ms ← Cache L1
Teste 2: Cache Miss → Cache Hit
GET /api/produtos/456
Response Time: 150ms ← Busca no banco
GET /api/produtos/456 (segunda chamada)
Response Time: 0.2ms ← Cache L1
Teste 3: Sincronização Between Instâncias
Instância A: POST /api/produtos/123 (atualiza produto)
Backplane: Sincroniza remoção automaticamente
Instância B: GET /api/produtos/123
Response: Dados atualizados! ✅
Resultados Obtidos
Performance
- 95% redução no tempo de resposta para dados em cache
- 80% redução na carga do banco de dados
- Sub-segundo response time para a maioria das consultas
Resiliência
- 100% uptime mesmo com falhas do Redis
- Zero downtime durante manutenções do cache
- Fail-safe mantém aplicação funcionando
Escalabilidade
- Sincronização automática entre N instâncias
- Load balancer friendly (cache consistente)
- Zero configuração adicional para novas instâncias
Configuração de Fallback
Para desenvolvimento ou quando Redis não está disponível:
// Fallback automático para cache apenas em memória
try
{
// Configuração com Redis (produção)
ConfigurarFusionCacheComRedis();
}
catch (Exception)
{
// Fallback para apenas memória (desenvolvimento)
services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions
{
Duration = TimeSpan.FromMinutes(10)
// Sem DistributedCache nem Backplane
});
}
Conclusão
O FusionCache oferece uma solução para cache em aplicações .NET 8:
Zero breaking changes no código existente
Performance extrema com cache multi-nível
Sincronização automática entre instâncias
Resiliência total a falhas de infraestrutura
Configuração flexível por tipo de dados
Esta implementação é ideal para:
- APIs de alta performance
- Aplicações distribuídas
- Microserviços que precisam de cache consistente
Com essas configurações, você consegue ter cache que melhora drasticamente a performance e reduz a carga no banco de dados, enquanto mantém dados sempre sincronizados entre todas as instâncias!
Simples né? Com apenas essas configurações você consegue ter uma solução de cache robusta, performática e resiliente.
Espero que tenham gostado e até a próxima!
#backend #cache #fusionCache #dotnet #braziliandevs #performance #redis #valkey
This content originally appeared on DEV Community and was authored by Bruno Silva