Redis Cache (Cache Aside)
Cache aside: por que e como
Aplicando cache-aside no customer-service com fallback seguro para Postgres.
Nesta aula você vai
- Implementar cache-aside no fluxo de leitura de cliente
- Registrar cache hit e cache miss
- Garantir fallback para banco quando Redis falhar
Cache aside: por que e como
Objetivos
- Reduzir latência de leitura no
customer-service. - Preservar consistência do banco como fonte de verdade.
- Preparar base para TTL e invalidação da próxima aula.
Pré-requisitos
customer-service(Java/Spring) rodando.- Redis ativo em
redis://redis:6379. - Spring Data Redis configurado no projeto.
Conceito
No padrão cache-aside, a aplicação tenta ler do cache primeiro. Se não achar, lê do banco, retorna para o cliente e grava no cache. Escritas continuam indo para o banco, com invalidação posterior.
Estrutura de arquivos
services/customer-service/src/main/java/br/com/sistemasamo/customer/
├── cache/CustomerCacheRepository.java
├── service/CustomerService.java
├── repository/CustomerRepository.java
└── web/CustomerController.java
Passo a passo
- Repositório de cache em
CustomerCacheRepository.java:
package br.com.sistemasamo.customer.cache;
import br.com.sistemasamo.customer.web.CustomerResponse;
import java.time.Duration;
import java.util.Optional;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class CustomerCacheRepository {
private static final Duration TTL = Duration.ofMinutes(10);
private final StringRedisTemplate redis;
private final ObjectMapper objectMapper;
public CustomerCacheRepository(StringRedisTemplate redis, ObjectMapper objectMapper) {
this.redis = redis;
this.objectMapper = objectMapper;
}
public Optional<CustomerResponse> getById(String customerId) {
try {
String raw = redis.opsForValue().get(key(customerId));
if (raw == null) return Optional.empty();
return Optional.of(objectMapper.readValue(raw, CustomerResponse.class));
} catch (Exception e) {
return Optional.empty();
}
}
public void put(CustomerResponse value) {
try {
redis.opsForValue().set(
key(value.id()),
objectMapper.writeValueAsString(value),
TTL
);
} catch (Exception ignored) {
// cache não pode quebrar fluxo principal
}
}
private String key(String customerId) {
return "customer-service:customer:" + customerId;
}
}
- Integre no serviço em
CustomerService.java:
public CustomerResponse getCustomerById(String id) {
var fromCache = cacheRepository.getById(id);
if (fromCache.isPresent()) {
meterRegistry.counter("customer_cache_hit_total").increment();
return fromCache.get();
}
meterRegistry.counter("customer_cache_miss_total").increment();
var entity = customerRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Cliente não encontrado: " + id));
var response = mapper.toResponse(entity);
cacheRepository.put(response);
return response;
}
Como testar
docker compose up -d customer-service redis postgres
curl -s http://localhost:8081/customers/c-1 | jq
curl -s http://localhost:8081/customers/c-1 | jq
curl -s http://localhost:8081/actuator/prometheus | rg "customer_cache_(hit|miss)_total"
A primeira chamada tende a gerar miss; a segunda, hit.
Dicas de projeto
- Sempre proteja cache com
try/catch. - Use prefixo de chave por serviço para evitar colisão.
- Métricas de hit/miss são obrigatórias para justificar o cache.
Erros comuns
- Lançar exceção de Redis para o cliente final.
- Cachear entidade JPA completa em vez de DTO de resposta.
- Não medir impacto de latência.
Resumo
Você implementou cache-aside real no customer-service com fallback seguro e observabilidade de hit/miss.