Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud


🗺️ Mapa Conceitual do Tópico

flowchart TD
    A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
    B --> C["Controller / Handler"]
    C --> D["Service Layer (Regras de Negócio)"]
    D --> E["Repository / ORM (Persistência)"]
    E --> F["Banco de Dados / Cache"]

    subgraph ARQ["Arquitetura do Capítulo"]
        G["Conceito: Cache Corporativo com Spring Data Redis (@Cacheable)"]
        H["Segurança, Validação e Resiliência"]
        I["Alta Performance e Escalabilidade"]
    end

    D --> ARQ

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px

🏛️ 1. Fundamentos Técnicos de Cache Corporativo com Spring Data Redis (@Cacheable)

Por que cache distribuído. Um cache em memória local (ConcurrentHashMap, Caffeine) funciona bem para uma única instância, mas em uma aplicação escalada horizontalmente com múltiplas réplicas, cada instância teria seu próprio cache isolado — inconsistente e ineficiente (o mesmo dado é buscado no banco N vezes, uma por instância). O Redis resolve isso como um armazenamento chave-valor em memória, compartilhado por todas as instâncias da aplicação através da rede, funcionando como uma camada de cache distribuída e centralizada.

Abstração de cache do Spring (spring-boot-starter-cache). @EnableCaching ativa um mecanismo de proxy AOP: para cada chamada a um método anotado, o Spring intercepta a invocação antes que ela realmente execute. @Cacheable(value = "produtos", key = "#id") funciona como uma memoização declarativa — na primeira chamada com um dado id, o método executa normalmente (ex.: consulta ao banco) e o retorno é gravado no Redis sob a chave produtos::<id>; em chamadas subsequentes com o mesmo id, o proxy intercepta e retorna o valor cacheado sem executar o corpo do método. A expressão key usa SpEL para compor a chave a partir dos parâmetros do método.

Invalidação: o problema mais difícil de cache. Um cache desatualizado é pior que não ter cache — serve dado obsoleto silenciosamente. @CacheEvict(value = "produtos", key = "#id") remove a entrada correspondente do Redis, tipicamente aplicado em métodos de atualização/remoção, forçando a próxima leitura a repopular o cache com o dado fresco (allEntries = true limpa a coleção inteira, útil quando uma operação afeta múltiplas chaves de forma imprevisível). @CachePut é diferente de ambos: sempre executa o método e atualiza o cache com o novo retorno, sem nunca servir o valor antigo — útil quando se quer garantir que o cache reflita imediatamente uma escrita, sem pagar o preço de invalidar e esperar a próxima leitura repopular.

Serialização. Por padrão, o RedisCacheManager pode usar serialização binária Java (JdkSerializationRedisSerializer), que exige que as classes armazenadas implementem Serializable e é frágil a mudanças de versão de classe, além de ilegível para debug direto no Redis. Configurar GenericJackson2JsonRedisSerializer grava os valores como JSON legível, resolvendo ambos os problemas e permitindo interoperabilidade com clientes não-Java. O TTL (time to live) de cada entrada é configurado via RedisCacheConfiguration.entryTtl(Duration...) — sem TTL, entradas nunca expiram, acumulando dados obsoletos e memória indefinidamente.

Cache stampede (ou “dogpile effect”). Quando uma chave de cache muito acessada expira, múltiplas requisições concorrentes podem, no mesmo instante, encontrar cache-miss e disparar simultaneamente a mesma consulta cara ao banco — anulando o benefício do cache exatamente no pico de carga. Mitigações comuns incluem: escalonar o TTL com um pequeno jitter aleatório (para que chaves relacionadas não expirem todas ao mesmo tempo), usar um lock distribuído para que apenas uma requisição recalcule o valor enquanto as demais aguardam ou servem um valor levemente obsoleto (stale-while-revalidate), ou pré-aquecer (warm-up) chaves críticas antes que expirem.

Lock distribuído com Redisson. Além de cache, o Redis serve como coordenador de concorrência entre instâncias. O Redisson (um cliente Redis de alto nível) expõe RLock, implementando o algoritmo Redlock: rLock.tryLock(tempoDeEspera, tempoDeExpiracao, TimeUnit...) tenta adquirir um lock distribuído, essencial para proteger seções críticas que não podem ser executadas concorrentemente por réplicas diferentes (ex.: decrementar estoque). O tempo de expiração do lock é uma rede de segurança: se a instância que o adquiriu travar antes de liberá-lo, o lock expira sozinho, evitando um deadlock permanente no cluster.


💻 2. Código de Demonstração Corporativo

package com.empresa.api.config;

import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;

@Configuration
public class RedisCacheConfig {

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10)) // sem TTL, entradas nunca expiram
            .disableCachingNullValues()
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }

    // Customiza TTLs diferentes por nome de cache, sobrepondo o padrão acima.
    @Bean
    public RedisCacheManagerBuilderCustomizer cacheManagerCustomizer() {
        return builder -> builder
            .withCacheConfiguration("produtos", RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)))
            .withCacheConfiguration("cotacoes", RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(30))); // dado volátil: TTL curto
    }
}
package com.empresa.api.service;

import com.empresa.api.model.Produto;
import com.empresa.api.repository.ProdutoRepository;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ProdutoCacheService {

    private final ProdutoRepository produtoRepository;

    public ProdutoCacheService(ProdutoRepository produtoRepository) {
        this.produtoRepository = produtoRepository;
    }

    @Cacheable(value = "produtos", key = "#id")
    public Produto buscarPorId(Long id) {
        // Só executa em cache-miss: primeira consulta ou após expiração/invalidação.
        return produtoRepository.buscarPorId(id);
    }

    @CachePut(value = "produtos", key = "#produto.id()")
    public Produto atualizar(Produto produto) {
        produtoRepository.salvar(produto);
        return produto; // cache já sai atualizado com o novo valor, sem esperar o próximo miss
    }

    @CacheEvict(value = "produtos", key = "#id")
    public void remover(Long id) {
        produtoRepository.remover(id);
    }
}