404 Not Found

404 Not Found


nginx

Mecanismo de Cache

O cache é o primeiro passo na otimização de desempenho—para dados que são lidos frequentemente mas escritos raramente, armazená-los em cache uma vez pode economizar mil consultas.

1. O Que Você Vai Aprender


2. Uma História Real de um Engenheiro de Desempenho

(1) Dor: O banco de dados está sobrecarregado

No lançamento do produto, Charlie demonstrou o recurso de flash sale do OrderFlow, que instantaneamente acionou 500 mil requisições—todas inundaram o banco de dados com consultas de produtos populares. O pool de conexões MySQL foi esgotado, causando a queda de todo o sistema por 10 minutos e resultando em uma perda direta de 100 mil USD. A análise de Alice revelou que consultas de detalhes de produtos representavam 80% do total de requisições, ainda assim esses dados permaneciam virtualmente inalterados por uma hora inteira.

(2) Solução Usando Spring Cache

Você pode habilitar o cache com apenas uma anotação:

JAVA
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElseThrow();
}

A primeira consulta acessa o banco de dados; consultas subsequentes leem diretamente do cache.

(3) Resultado

Depois que Alice adicionou cache Caffeine aos produtos populares, 80% das consultas atingiram o cache, a carga do banco de dados foi reduzida em 5 vezes, e a latência P99 caiu de 500 ms para 5 ms, suportando com sucesso o próximo flash sale.


3. Sistema de Anotações do Spring Cache

(1) Anotações Principais

Descrição Função Cenários Típicos
@Cacheable Verificar o cache primeiro ao consultar; se encontrado, retornar imediatamente Operações de Consulta
@CachePut Executar o método e atualizar o cache Operação de atualização
@CacheEvict Limpar Cache Exclusão
@Caching Combinar Múltiplas Operações de Cache Cenários Complexos
@EnableCaching Habilitar Suporte a Cache Classe de Configuração
100%
graph TD
    A["Requisição do Cliente"] --> B{"@Cacheable<br/>Cache Hit?"}
    B -->|Sim| C["Retornar Dados em Cache"]
    B -->|Não| D["Executar Método<br/>Consultar Banco de Dados"]
    D --> E["Armazenar Resultado no Cache"]
    E --> F["Retornar Resultado"]

(1) ▶ Exemplo: Cache de Consulta com @Cacheable

JAVA
@Service
@EnableCaching
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Cacheable(value = "products", key = "#id")
    public Product getProduct(Long id) {
        log.info("Cache miss, consultando banco de dados para o produto {}", id);
        return productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product", id));
    }

    @Cacheable(value = "product-list", key = "#keyword + '-' + #page + '-' + #size")
    public Page<Product> searchProducts(String keyword, int page, int size) {
        return productRepository.findByNameContaining(keyword,
            PageRequest.of(page, size));
    }
}

Saída:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: @CachePut atualiza o cache

JAVA
@CachePut(value = "products", key = "#result.id")
public Product updateProduct(Long id, UpdateProductRequest request) {
    Product product = getProduct(id);
    product.setName(request.name());
    product.setPrice(request.price());
    product.setStock(request.stock());
    return productRepository.save(product);
}

Saída:

TEXT
// Execução bem-sucedida

(3) ▶ Exemplo: @CacheEvict limpa o cache

JAVA
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
    productRepository.deleteById(id);
}

@CacheEvict(value = "product-list", allEntries = true)
public void clearProductListCache() {
    log.info("Limpadas todas as entradas de cache da lista de produtos");
}

Saída:

TEXT
// Execução bem-sucedida
Anotação Executar Método Atualizar Cache Operações Aplicáveis
@Cacheable Executar em cache miss Atualizar em cache miss Consulta
@CachePut Sempre Executar Sempre Atualizar Atualização
@CacheEvict Sempre Executar Limpar Cache Especificado Exclusão

4. Estratégia de Chave de Cache

(1) Regras de Geração de Chave

Estratégia de Chave Expressão SpEL Exemplo Gerado
Parâmetro Único key = "#id" products::123
Combinação Multi-parâmetro key = "#keyword + '-' + #page" product-list::laptop-0
Propriedade de Objeto key = "#request.category" products::electronics
Padrão Não especificado O hashCode de todas as combinações de parâmetros

(1) ▶ Exemplo: KeyGenerator Personalizado

JAVA
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean("customKeyGenerator")
    public KeyGenerator customKeyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getSimpleName()).append(":");
            sb.append(method.getName()).append(":");
            for (Object param : params) {
                sb.append(param != null ? param.toString() : "null").append(".");
            }
            return sb.toString();
        };
    }
}

// Uso
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public Product getProduct(Long id) { /* ... */ }

Saída:

TEXT
// Execução bem-sucedida

(2) Filtragem Condicional

(2) ▶ Exemplo: condition e unless

JAVA
// Armazenar em cache apenas produtos com preço > 100
@Cacheable(value = "expensive-products", key = "#id",
    condition = "#id != null")
public Product getExpensiveProduct(Long id) { /* ... */ }

// Não armazenar em cache se o resultado tem stock == 0
@Cacheable(value = "products", key = "#id",
    unless = "#result.stock == 0")
public Product getProduct(Long id) { /* ... */ }

Saída:

TEXT
// Execução bem-sucedida
Parâmetro Momento de Execução Significado
condition Antes do método ser executado Armazenar em cache apenas se as condições forem atendidas
unless Após o método ser executado Se a condição for atendida, não armazenar em cache

5. Configuração de Cache Local Caffeine

(1) Características do Caffeine

Característica Descrição
Alto Desempenho 30% mais throughput que o Guava Cache
Atualização Assíncrona Suporta atualização assíncrona em segundo plano de cache expirado
Estatísticas Taxa de Acerto, Tempo de Carregamento, etc.
Window TinyLFU Algoritmo de Evicção Ótimo

(1) ▶ Exemplo: Configuração de Cache Caffeine

XML
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

Saída:

TEXT
// Execução bem-sucedida
JAVA
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(caffeineCacheBuilder());
        return manager;
    }

    @Bean
    public Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(30))   // TTL: 30 min após escrita
            .expireAfterAccess(Duration.ofHours(1))      // Evict após 1 hora ocioso
            .maximumSize(10_000)                         // Máx 10.000 entradas
            .recordStats();                              // Habilitar estatísticas
    }

    // Configuração por cache
    @Bean
    public CacheManager multiCacheManager() {
        Map<String, CaffeineCache> caches = Map.of(
            "products", buildCache(Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofHours(1))
                .maximumSize(5_000)),
            "product-list", buildCache(Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofMinutes(10))
                .maximumSize(1_000))
        );
        return new SimpleCacheManager() {{
            setCaches(caches.values());
        }};
    }

    private CaffeineCache buildCache(Caffeine<Object, Object> builder) {
        return new CaffeineCache("cache", builder.build());
    }
}
Opção de Configuração Descrição Valor Recomendado
expireAfterWrite Tempo de expiração após escrita 30 min - 1 h
expireAfterAccess Tempo de expiração após último acesso 1h - 24h
maximumSize Número máximo de entradas de cache Ajustar com base na memória
recordStats Habilitar Estatísticas Habilitar em Ambiente de Desenvolvimento

6. Penetração e Avalanche de Cache

(1) Problemas Comuns de Cache

Problema Causa Consequência Solução
Penetração de Cache Consultar Dados Inexistentes Consultar o Banco de Dados Toda Vez Armazenar Valores Nulos / Filtros de Bloom
Avalanche de Cache Surge de requisições no exato momento em que uma chave quente expira Pico repentino de carga no banco de dados Lock de exclusão mútua / Nunca expira + atualização assíncrona
Avalanche em Massa Número Massivo de Chaves Expirando Simultaneamente Aumento Repentino na Carga do Banco de Dados Tempo de Expiração com Deslocamento Aleatório
100%
graph TD
    A["Penetração de Cache<br/>Consultar dados inexistentes"] --> B["Solução: Armazenar valor nulo"]
    C["Avalanche de Cache<br/>Chave quente expira"] --> D["Solução: Lock mutex / atualização assíncrona"]
    E["Avalanche em Massa<br/>Muitas chaves expiram de uma vez"] --> F["Solução: Deslocamento aleatório de TTL"]

(1) ▶ Exemplo: Armazenar valores nulos para prevenir penetração

JAVA
@Cacheable(value = "products", key = "#id", unless = "#result == null && #result != ''")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElse(null);
}

// Alternativa: Armazenar nulo com TTL curto
@Bean
public CacheManager cacheManager() {
    return new CaffeineCacheManager() {{
        setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(5))
            .maximumSize(10_000));
    }};
}

Saída:

TEXT
// Execução bem-sucedida

7. Exemplo Abrangente: Implementação Completa do Cache de Produtos do OrderFlow

JAVA
// CacheConfig.java
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        Map<String, Cache> caches = new HashMap<>();
        caches.put("products", Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofHours(1))
            .maximumSize(5_000)
            .recordStats()
            .build());
        caches.put("product-list", Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(10))
            .maximumSize(500)
            .build());
        caches.put("product-stats", Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(5))
            .maximumSize(100)
            .build());

        SimpleCacheManager manager = new SimpleCacheManager();
        manager.setCaches(caches.values().stream()
            .map(c -> new CaffeineCache(c.getClass().getName(), c))
            .toList());
        return manager;
    }
}

// ProductService.java com cache
@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Cacheable(value = "products", key = "#id")
    public Product getProduct(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product", id));
    }

    @Cacheable(value = "product-list", key = "#keyword + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
    public Page<Product> searchProducts(String keyword, Pageable pageable) {
        return productRepository.findByNameContaining(keyword, pageable);
    }

    @CachePut(value = "products", key = "#result.id")
    @CacheEvict(value = "product-list", allEntries = true)
    public Product updateProduct(Long id, String name, BigDecimal price, Integer stock) {
        Product product = getProduct(id);
        product.setName(name);
        product.setPrice(price);
        product.setStock(stock);
        return productRepository.save(product);
    }

    @CacheEvict(value = {"products", "product-list"}, allEntries = true)
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
}

❓ Perguntas Frequentes

P O @Cacheable funciona quando métodos da mesma classe chamam uns aos outros?
R Não, não funciona. Assim como @Transactional, @Cacheable depende de proxies AOP, e chamadas entre métodos da mesma classe não passam pelo proxy. Você precisa separar as classes ou injetar um proxy.
P Qual escolher entre Caffeine e Redis para cache?
R Caffeine é um cache local (processo único); é extremamente rápido mas não suporta compartilhamento em cluster. Redis é um cache distribuído; todas as instâncias compartilham dados, mas incorre em overhead de rede. Tipicamente, os dois são usados em combinação (L1 Caffeine + L2 Redis).
P Como é garantida a consistência entre o cache e o banco de dados?
R Estratégias comuns: 1) Cache Aside (atualizar o banco de dados primeiro, depois deletar o cache); 2) Write Through (escrever no cache e banco sincronamente); 3) Write Behind (escrever no cache primeiro, depois no banco assincronamente). Recomendamos Cache Aside combinado com dupla deleção adiada.
P Qual é a diferença entre maximumSize e maximumWeight?
R maximumSize limita o número de entradas, enquanto maximumWeight limita o peso total (requer especificar um weigher). Se os tamanhos dos objetos em cache variam significativamente, usar maximumWeight é mais preciso.
P Como monitorar a taxa de acerto do cache?
R Após habilitar recordStats() no Caffeine, você pode obter a taxa de acerto usando Cache.stats(). Combine isso com Micrometer para registrar as métricas no Prometheus e visualizá-las no Grafana.
P Podem @CachePut e @Cacheable ser usados no mesmo método?
R Isso não é recomendado. @Cacheable pode pular a execução do método (quando há cache hit), enquanto @CachePut sempre executa o método e atualiza o cache; a semântica dos dois está em conflito.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (Dificuldade: ⭐): Adicione o cache @Cacheable à consulta de produtos no OrderFlow e verifique se o log SQL não é impresso na segunda consulta (cache hit).

  2. Problema Avançado (Dificuldade ⭐⭐): Configure o gerenciador de cache Caffeine para definir diferentes TTLs e capacidades máximas para diferentes espaços de cache. Implemente @CachePut para atualizar o cache quando um produto for atualizado e @CacheEvict para limpar o cache quando um produto for deletado.

  3. Desafio (Dificuldade: ⭐⭐⭐): Implemente um cache de dois níveis (L1: Caffeine + L2: Redis). Crie um CacheResolver personalizado que determine se deve usar o cache local ou distribuído com base no nome do cache. Considere a estratégia de consistência para o cache de dois níveis.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%