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