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
@EnableCachinge Anotações@Cacheable/@CachePut/@CacheEvict- Estratégias de chave de cache e filtragem condicional "condition" / "unless"
- Configuração de Cache Local Caffeine: TTL / Capacidade Máxima / Política de Evicção
- Estratégias para Abordar Problemas de Penetração e Avalanche de Cache
- Alice adicionou cache Caffeine a uma lista de produtos populares que recebe milhões de visitas diárias
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:
@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 |
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
@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:
// Execução bem-sucedida
(2) ▶ Exemplo: @CachePut atualiza o cache
@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:
// Execução bem-sucedida
(3) ▶ Exemplo: @CacheEvict limpa o cache
@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:
// 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
@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:
// Execução bem-sucedida
(2) Filtragem Condicional
(2) ▶ Exemplo: condition e unless
// 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:
// 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
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
Saída:
// Execução bem-sucedida
@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 |
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
@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:
// Execução bem-sucedida
7. Exemplo Abrangente: Implementação Completa do Cache de Produtos do OrderFlow
// 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
maximumSize e maximumWeight?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.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.📖 Resumo
@CacheableVerificar cache → Se cache miss, consultar banco de dados → Escrever no cache@CachePutSempre executa o método e atualiza o cache; adequado para operações de atualização@CacheEvictLimpar o cache;allEntries = trueLimpar todo o cache- Política de chave expressa usando expressões SpEL;
condition/unlesscondições de filtragem de cache - Configurar TTL, capacidade máxima e política de evicção do Caffeine para um cache local de alto desempenho
- Três Maiores Problemas de Cache: Penetração de Cache (armazenar valores nulos), Avalanche de Cache (locks de exclusão mútua), Avalanche em Massa (TTL aleatório)
📝 Exercícios
-
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). -
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
@CachePutpara atualizar o cache quando um produto for atualizado e@CacheEvictpara limpar o cache quando um produto for deletado. -
Desafio (Dificuldade: ⭐⭐⭐): Implemente um cache de dois níveis (L1: Caffeine + L2: Redis). Crie um
CacheResolverpersonalizado 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.



