Redis Cache (Cache Aside)

Cache aside: por que e como

Aplicando cache-aside no customer-service com fallback seguro para Postgres.

Avançado 30 min 25 pontos Leitura 0%

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

  1. 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;
  }
}
  1. 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.