Dicas de caching eficiente em Redis
1. Fundamentos do Caching com Redis
O Redis oferece múltiplas estruturas de dados que devem ser escolhidas conforme o padrão de acesso. Para cache de objetos simples, use Strings. Para armazenar campos de um registro que são acessados individualmente, prefira Hashes — isso reduz o tráfego de rede e permite atualizações parciais sem desserializar o objeto inteiro.
# Exemplo: Armazenar perfil de usuário como Hash
HSET user:1001 nome "Maria" email "maria@exemplo.com" ultimo_acesso "2025-03-20"
HGET user:1001 email
# Resultado: "maria@exemplo.com"
Para rankings e top-N, utilize Sorted Sets. Para listas de permissões ou tags, Sets oferecem operações O(1) de pertinência.
As políticas de expiração são cruciais. Defina TTL (time-to-live) para cada chave. Em cenários de memória limitada, configure maxmemory-policy como allkeys-lru (remove as chaves menos usadas recentemente) ou volatile-lru (remove apenas chaves com TTL definido).
A serialização impacta diretamente a performance. JSON é legível mas verboso. MessagePack reduz o tamanho em ~30% comparado ao JSON. Protocol Buffers oferece a melhor compressão e velocidade de desserialização, ideal para microsserviços.
# Exemplo de objeto serializado em JSON vs MessagePack
# JSON: {"id":1,"nome":"João","saldo":1500.50} -> 38 bytes
# MessagePack: 0x81 0x02 ... -> aproximadamente 26 bytes
2. Estratégias de Cache Pattern
O padrão Cache-aside (Lazy Loading) é o mais comum: a aplicação verifica o cache primeiro; em caso de miss, busca no banco de dados, armazena no cache e retorna. Implemente tratamento para cache miss com bloqueio mínimo.
# Pseudocódigo: Cache-aside com mutex simples
function getProduto(id):
produto = redis.get("produto:" + id)
if produto is None:
lock = redis.lock("lock:produto:" + id, timeout=5)
if lock.acquire():
produto = db.buscarProduto(id)
redis.setex("produto:" + id, 3600, produto)
lock.release()
else:
sleep(50ms)
return getProduto(id) # retry
return produto
Write-through atualiza cache e banco simultaneamente, garantindo consistência imediata mas aumentando latência da escrita. Write-behind (ou write-back) acumula escritas no cache e as persiste em lote, oferecendo alta performance com risco de perda de dados em falhas.
Cache warming é essencial após reinicializações: pré-carregue dados críticos (catálogos de produtos, configurações) usando scripts que consultam o banco e populam o Redis antes de aceitar tráfego.
3. Otimização de Memória e Performance
Ative a compressão LZF no Redis (configuração activerehashing yes e use redis-cli --lzf-compress para dados grandes). Para valores pequenos, prefira encoding nativo: inteiros são armazenados como strings otimizadas.
Evite chaves muito longas (>128 caracteres) — cada byte em nomes de chave consome RAM. Prefira namespaces curtos: usr:1001:profile em vez de usuario:1001:dados_completos_do_perfil.
Valores muito grandes (>10KB) devem ser fragmentados ou comprimidos. Use Pipeline para enviar múltiplos comandos em uma única conexão, reduzindo round-trips de rede.
# Pipeline com redis-py
pipe = redis.pipeline()
for i in range(1000):
pipe.set(f"chave:{i}", f"valor:{i}")
pipe.execute() # 1 round-trip para 1000 operações
Transações com MULTI/EXEC garantem atomicidade sem bloqueio. Combine com WATCH para controle de concorrência otimista.
4. Prevenção de Problemas Comuns
Thundering herd ocorre quando múltiplas requisições simultâneas detectam cache miss e todas tentam recalcular o mesmo dado. Use mutex distribuído (SET NX com TTL curto) para que apenas um processo recalcule.
# Mutex distribuído com SET NX
chave_bloqueio = "lock:relatorio:vendas"
if redis.setnx(chave_bloqueio, "1", ex=30):
try:
dados = calcularRelatorioVendas()
redis.setex("cache:relatorio:vendas", 600, dados)
finally:
redis.delete(chave_bloqueio)
else:
sleep(100ms)
return redis.get("cache:relatorio:vendas")
Cache stampede é semelhante, mas ocorre quando um cache expira e muitas requisições tentam recalculá-lo. Solução: recálculo proativo — antes da expiração real, inicie um refresh em background quando o TTL residual for menor que 20% do TTL total.
Stale cache pode ser aceitável em alguns cenários. Implemente background refresh: uma thread verifica periodicamente a idade do cache e, se estiver próxima da expiração, atualiza o valor em segundo plano, mantendo o cache sempre "quente".
5. Padrões de Invalidação de Cache
Time-based invalidation (TTL) é simples mas impreciso. Event-based invalidation é mais eficiente: quando um registro é alterado no banco, publique uma mensagem (Redis Pub/Sub ou fila) que remove ou atualiza a chave correspondente.
Cache tagging permite invalidar grupos de chaves relacionados. Armazene tags como um Set e, ao invalidar, remova todas as chaves associadas à tag.
# Tagging: associar chaves a categorias
SADD "tag:esportes" "produto:101" "produto:102" "produto:103"
# Invalidar todos os produtos de esportes
SMEMBERS "tag:esportes" -> ["produto:101", "produto:102", "produto:103"]
DEL "produto:101", "produto:102", "produto:103"
DEL "tag:esportes"
Versionamento de chaves adiciona um número de versão ao nome da chave (ex: produto:v2:101). Quando o esquema de dados muda, incremente a versão — o cache antigo é automaticamente ignorado.
6. Monitoramento e Métricas
Monitore hit rate (ideal >90%) e miss rate. Latência média de operações deve ser <5ms para gets e <10ms para sets. Use INFO stats no Redis para obter essas métricas.
Ative o Slow Log para identificar comandos lentos:
CONFIG SET slowlog-log-slower-than 10000 # comandos >10ms
SLOWLOG GET 10 # últimos 10 comandos lentos
Ferramentas como Redis Insight oferecem dashboard visual. Para produção, configure Prometheus + Grafana com o redis_exporter, monitorando memória usada, hits/misses, conexões ativas e fragmentação.
7. Casos de Uso Avançados
Rate limiting com sliding window usando Sorted Sets:
# Rate limit: máximo 100 requisições por minuto por IP
chave = "rate:ip:" + ip_usuario
agora = timestamp_em_micros
redis.zadd(chave, {str(agora): agora})
redis.zremrangebyscore(chave, 0, agora - 60000000) # remove >1 minuto
redis.expire(chave, 60)
quantidade = redis.zcard(chave)
if quantidade > 100:
retornar "429 Too Many Requests"
Sessões distribuídas armazenam dados de autenticação com TTL igual ao tempo de expiração do token JWT. Use Hashes para armazenar claims e atualizar o TTL a cada requisição.
Cache de queries complexas: para agregações SQL pesadas (relatórios mensais, somatórios), armazene o resultado com TTL de horas. Invalide via evento quando os dados base forem alterados.
# Cache de relatório mensal
chave = "relatorio:vendas:2025-03"
dados = redis.get(chave)
if not dados:
dados = db.query("SELECT SUM(valor) FROM vendas WHERE mes='2025-03'")
redis.setex(chave, 7200, dados) # 2 horas de cache
Referências
- Documentação oficial do Redis - Estruturas de dados — Guia completo sobre Strings, Hashes, Sets, Sorted Sets e seus casos de uso ideais.
- Redis: Cache Patterns - Cache-Aside — Explicação detalhada do padrão Lazy Loading com exemplos práticos de implementação.
- Redis Memory Optimization — Técnicas oficiais para reduzir uso de memória, compressão LZF e encoding eficiente.
- Prevenindo Thundering Herd no Redis — Artigo do Facebook Engineering sobre estratégias de mutex e locks distribuídos para evitar sobrecarga.
- Monitorando Redis com Prometheus e Grafana — Guia oficial de configuração do redis_exporter e dashboards para métricas de hit rate e latência.
- Redis Rate Limiting Patterns — Implementações de sliding window e contadores atômicos para controle de tráfego.