404 Not Found

404 Not Found


nginx

Spring Data Redis

Redis é o canivete suíço do caching—com suas cinco estruturas de dados—String, Hash, List, Set e ZSet—cobre tudo, desde cache até sessões e mensageria.

1. O Que Você Vai Aprender


2. Uma História Real de um Arquiteto de Cluster

(1) Dor: Caches Entre Múltiplas Instâncias Não São Compartilhados

Depois que Alice escalou o OrderFlow de uma única instância para três instâncias, surgiram problemas com o cache local Caffeine: a Instância A atualizou os preços dos produtos e atualizou o cache local, mas os caches locais nas Instâncias B e C ainda continham os preços antigos, resultando em preços inconsistentes exibidos aos usuários. Bob tentou usar Redis para compartilhar sessões, mas não sabia como integrar o Spring Cache com o Redis.

(2) Solução Usando Spring Data Redis

Como cache distribuído, o Redis compartilha o seguinte entre todas as instâncias:

JAVA
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElseThrow();
}
// Cache armazenado no Redis, compartilhado entre todas as instâncias

(3) Resultado

Depois que Bob substituiu o Caffeine pelo Redis como cache distribuído, as três instâncias mantiveram caches consistentes, e as atualizações de preços dos produtos foram sincronizadas em tempo real. Ao mesmo tempo, o Pub/Sub do Redis foi usado para implementar notificações de eventos de pedidos, o Redis foi usado para armazenar a blacklist de JWT, e o status de login foi compartilhado entre múltiplas instâncias.


3. Trabalhando com Estruturas de Dados do Redis

(1) Cinco Principais Estruturas de Dados

Tipo Propósito Cenários Típicos
String Pares chave-valor simples Cache, contadores, sessões
Hash Propriedades de Objeto Detalhes de Produto, Informações de Usuário
List Lista ordenada Fila de mensagens, lista de recentes
Set Conjunto Não Ordenado Tags, Amigos em Comum
ZSet Conjunto Ordenado Rankings, Filas de Latência

(1) ▶ Exemplo: Operações Básicas do RedisTemplate

JAVA
@Service
public class RedisOperationService {

    private final StringRedisTemplate redis;

    public RedisOperationService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    // Operações String
    public void cacheProduct(Long id, String json) {
        redis.opsForValue().set("product:" + id, json, Duration.ofHours(1));
    }

    public String getProduct(Long id) {
        return redis.opsForValue().get("product:" + id);
    }

    // Contador
    public Long incrementOrderCount() {
        return redis.opsForValue().increment("stats:daily-orders");
    }

    // Operações Hash
    public void saveProductDetail(Long id, Map<String, String> details) {
        redis.opsForHash().putAll("product:detail:" + id, details);
    }

    public Map<Object, Object> getProductDetail(Long id) {
        return redis.opsForHash().entries("product:detail:" + id);
    }

    // Operações List - Pedidos recentes
    public void addRecentOrder(Long orderId) {
        redis.opsForList().leftPush("recent:orders", orderId.toString());
        redis.opsForList().trim("recent:orders", 0, 99);  // Manter últimos 100
    }

    public List<String> getRecentOrders() {
        return redis.opsForList().range("recent:orders", 0, -1);
    }

    // Operações Set - Tags de produtos
    public void addProductTag(Long productId, String tag) {
        redis.opsForSet().add("product:tags:" + productId, tag);
    }

    public Set<String> getProductTags(Long productId) {
        return redis.opsForSet().members("product:tags:" + productId);
    }

    // Operações ZSet - Ranking de vendas
    public void updateSalesRank(Long productId, double salesCount) {
        redis.opsForZSet().add("sales:ranking", productId.toString(), salesCount);
    }

    public List<String> getTopSellingProducts(int limit) {
        return redis.opsForZSet()
            .reverseRange("sales:ranking", 0, limit - 1)
            .stream().toList();
    }
}

Saída:

TEXT
// Execução bem-sucedida

4. Integrando @Cacheable com Redis

(1) Configuração de Cache Redis

(1) ▶ Exemplo: Configuração do RedisCacheManager

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Saída:

TEXT
// Execução bem-sucedida
YAML
spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      lettuce:
        pool:
          max-active: 20
          max-idle: 10
          min-idle: 5
JAVA
@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .disableCachingNullValues();

        Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
            "products", config.entryTtl(Duration.ofHours(1)),
            "product-list", config.entryTtl(Duration.ofMinutes(10)),
            "order-stats", config.entryTtl(Duration.ofMinutes(5))
        );

        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(cacheConfigs)
            .transactionAware()
            .build();
    }
}
Lettuce vs. Jedis Lettuce Jedis
Modelo de Conexão Assíncrono Não-Bloqueante (Netty) Síncrono Bloqueante
Thread-safe Sim (conexão única compartilhada por múltiplas threads) Não (requer pool de conexões)
Desempenho Melhor para cenários de alta concorrência Adequado para cenários simples
Padrão Spring Boot ✅ Padrão Deve ser trocado manualmente

5. Publish/Subscribe do Redis

(1) Modelo de Mensagens Pub/Sub

100%
sequenceDiagram
    participant P as Publicador<br/>(OrderService)
    participant R as Redis<br/>Pub/Sub
    participant S1 as Assinante 1<br/>(NotificationService)
    participant S2 as Assinante 2<br/>(AnalyticsService)

    P->>R: PUBLISH order-events "ORDER_CREATED:123"
    R->>S1: Mensagem: "ORDER_CREATED:123"
    R->>S2: Mensagem: "ORDER_CREATED:123"

(1) ▶ Exemplo: Evento de Realização de Pedido

JAVA
@Service
public class OrderEventPublisher {

    private final StringRedisTemplate redis;

    public OrderEventPublisher(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public void publishOrderCreated(Long orderId) {
        redis.convertAndSend("order-events",
            "ORDER_CREATED:" + orderId);
    }

    public void publishOrderCancelled(Long orderId) {
        redis.convertAndSend("order-events",
            "ORDER_CANCELLED:" + orderId);
    }
}

Saída:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Assinando Eventos de Pedidos

JAVA
@Component
public class OrderEventSubscriber {

    private static final Logger log = LoggerFactory.getLogger(OrderEventSubscriber.class);

    @RedisListener(topic = "order-events")
    public void handleOrderEvent(String message) {
        String[] parts = message.split(":");
        String eventType = parts[0];
        Long orderId = Long.parseLong(parts[1]);

        switch (eventType) {
            case "ORDER_CREATED" -> log.info("Processando pedido criado: {}", orderId);
            case "ORDER_CANCELLED" -> log.info("Processando pedido cancelado: {}", orderId);
        }
    }
}

@Configuration
public class RedisPubSubConfig {

    @Bean
    public RedisMessageListenerContainer container(
            RedisConnectionFactory factory,
            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(listenerAdapter,
            new ChannelTopic("order-events"));
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(OrderEventSubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, "handleOrderEvent");
    }
}

Saída:

TEXT
// Execução bem-sucedida

6. Sessões Compartilhadas do Redis e Blacklists de JWT

(1) ▶ Exemplo: Implementação de Blacklist de JWT

JAVA
@Service
public class TokenBlacklistService {

    private final StringRedisTemplate redis;

    public TokenBlacklistService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public void blacklistToken(String tokenId, Duration remainingTtl) {
        redis.opsForValue().set(
            "token:blacklist:" + tokenId, "1", remainingTtl);
    }

    public boolean isBlacklisted(String tokenId) {
        return Boolean.TRUE.equals(
            redis.hasKey("token:blacklist:" + tokenId));
    }
}

Saída:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Limitação de Taxa Distribuída

JAVA
@Service
public class RateLimitService {

    private final StringRedisTemplate redis;

    public RateLimitService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public boolean isAllowed(String clientId, int maxRequests, Duration window) {
        String key = "rate-limit:" + clientId;
        Long count = redis.opsForValue().increment(key);
        if (count != null && count == 1) {
            redis.expire(key, window);
        }
        return count != null && count <= maxRequests;
    }
}

Saída:

TEXT
// Execução bem-sucedida
Casos de Uso Estruturas de Dados Redis Design de Chave
Cache Distribuído String / Hash cache:products:123
Blacklist de JWT String + TTL token:blacklist:tokenId
Limitação de Taxa Distribuída String + INCR + TTL rate-limit:clientId
Pedidos Recentes List + LPUSH + TRIM recent:orders
Mais Vendidos ZSet sales:ranking
Notificação de Eventos Pub/Sub order-events

7. Exemplo Abrangente: Integração Completa do OrderFlow com Redis

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

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(factory)
            .cacheDefaults(defaults)
            .withInitialCacheConfigurations(Map.of(
                "products", defaults.entryTtl(Duration.ofHours(1)),
                "product-list", defaults.entryTtl(Duration.ofMinutes(10))
            ))
            .build();
    }
}

// ProductService com cache Redis
@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));
    }

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

// OrderEventPublisher.java
@Service
public class OrderEventPublisher {
    private final StringRedisTemplate redis;
    public OrderEventPublisher(StringRedisTemplate redis) { this.redis = redis; }

    public void publishOrderCreated(Long orderId) {
        redis.convertAndSend("order-events", "ORDER_CREATED:" + orderId);
    }
}
YAML
# application.yml
spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      lettuce:
        pool:
          max-active: 20
          max-idle: 10

❓ Perguntas Frequentes

P Qual é a diferença entre RedisTemplate e StringRedisTemplate?
R StringRedisTemplate herda de RedisTemplate<String, String>; tanto chaves quanto valores são Strings, e usa StringRedisSerializer. RedisTemplate usa serialização JDK por padrão, que é menos legível. Recomendamos usar StringRedisTemplate ou um serializador personalizado.
P Como escolher entre cache Redis e cache Caffeine?
R Use Caffeine para uma única instância (mais rápido), e Redis para múltiplas instâncias (compartilhado). Melhor prática: Um sistema de cache de dois níveis com Caffeine como L1 e Redis como L2, onde o cache local é de curto prazo (1-5 minutos) e o cache Redis é de longo prazo (30-60 minutos).
P Mensagens do Pub/Sub do Redis podem ser perdidas?
R Sim. O Pub/Sub opera em um modelo de "dispare e esqueça", então mensagens são perdidas se os assinantes estiverem offline. Se você precisar de mensageria confiável, use Redis Streams ou uma fila de mensagens dedicada (como RabbitMQ ou Kafka).
P O Lettuce requer um pool de conexões?
R O Lettuce usa multiplexação de conexão única, então um pool de conexões geralmente não é necessário. No entanto, em cenários que envolvem operações bloqueantes de alta concorrência, configurar um pool de conexões pode melhorar o throughput. O Jedis deve ter um pool de conexões configurado.
P Como escolher um método de serialização para cache Redis?
R Recomendamos serialização JSON (GenericJackson2JsonRedisSerializer) por sua legibilidade e compatibilidade entre linguagens. Serialização JDK é ilegível e não recomendada. Você também pode usar StringRedisSerializer combinado com conversão JSON manual.
P Como lidar com falhas de conexão com o Redis?
R 1) Por padrão, o Spring Cache não lança exceção quando ocorre uma falha (recorre a uma consulta ao banco de dados); 2) Configure CacheErrorHandler para implementar tratamento de falha personalizado; 3) Use Redis Sentinel ou Cluster para garantir alta disponibilidade.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (Dificuldade ⭐): Configure o Spring Data Redis para o OrderFlow, use @Cacheable para armazenar o cache de produtos no Redis e verifique o compartilhamento de cache entre múltiplas instâncias.

  2. Exercício Avançado (Dificuldade: ⭐⭐): Implemente notificações de eventos de pedidos via Redis Pub/Sub—publique o evento ORDER_CREATED quando um pedido for realizado, e faça o serviço de notificação assiná-lo e enviar um e-mail. Implemente um recurso de revogação de token na blacklist de JWT.

  3. Desafio (Dificuldade: ⭐⭐⭐): Implemente um sistema de cache de dois níveis consistindo em L1 Caffeine e L2 Redis. Quando o cache Redis for atualizado, use Pub/Sub para notificar todas as instâncias a limparem seus caches Caffeine locais, garantindo consistência em ambos os 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%