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
RedisTemplateeStringRedisTemplateoperações em String / Hash / List / Set / ZSet@CacheableIntegrando Redis como backend de cache distribuído- Publish/Subscribe (Pub/Sub) do Redis para Notificações de Eventos de Pedidos
- Comparação de Configurações de Pool de Conexões Redis: Lettuce vs. Jedis
- Bob usa Redis para armazenar sessões em cache, permitindo que múltiplas instâncias do OrderFlow compartilhem o status de login
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:
@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
@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:
// Execução bem-sucedida
4. Integrando @Cacheable com Redis
(1) Configuração de Cache Redis
(1) ▶ Exemplo: Configuração do RedisCacheManager
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Saída:
// Execução bem-sucedida
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
@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
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
@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:
// Execução bem-sucedida
(2) ▶ Exemplo: Assinando Eventos de Pedidos
@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:
// Execução bem-sucedida
6. Sessões Compartilhadas do Redis e Blacklists de JWT
(1) ▶ Exemplo: Implementação de Blacklist de JWT
@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:
// Execução bem-sucedida
(2) ▶ Exemplo: Limitação de Taxa Distribuída
@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:
// 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
// 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);
}
}
# 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
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.StringRedisSerializer combinado com conversão JSON manual.CacheErrorHandler para implementar tratamento de falha personalizado; 3) Use Redis Sentinel ou Cluster para garantir alta disponibilidade.📖 Resumo
- As Cinco Principais Estruturas de Dados no Redis: String, Hash, List, Set e ZSet
@Cacheable+ RedisCacheManager para implementar um cache distribuído compartilhado por todas as instâncias- Lettuce é o cliente Redis padrão do Spring Boot; é assíncrono, não-bloqueante e thread-safe.
- O Pub/Sub do Redis implementa notificação de eventos, mas as mensagens são não confiáveis (mensagens podem ser perdidas se os assinantes estiverem offline).
- Use Redis String + TTL para a blacklist de JWT; use INCR + TTL para limitação de taxa distribuída
- Use Redis Sentinel ou Cluster em ambientes de produção para garantir alta disponibilidade
📝 Exercícios
-
Exercício Básico (Dificuldade ⭐): Configure o Spring Data Redis para o OrderFlow, use
@Cacheablepara armazenar o cache de produtos no Redis e verifique o compartilhamento de cache entre múltiplas instâncias. -
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.
-
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.



