Pular para o conteúdo
iauaiiauai — portal de tecnologia, IA e Cloud
CloudIntermediário

Cache com Redis: padrões que funcionam

Cache-aside, write-through, write-behind. Invalidação, stampede, hit rate, TTL. Código Python e Redis, quando não cachear, custos de replicação medidos.

Por Equipe iauai · 5 de agosto de 2026 · 12 min de leitura

Nesta página

O problema real

Consultar perfil de usuário no banco demora 80 ms (network latência + query). Sua API responde 100 requisições/segundo × 80 ms = 8 segundos de latência total. Cliente espera, browser carrega lento, usuário sai.

Aí coloca Redis. Mesma query agora demora 5 ms (rede local). Latência cai de 8s para 5s. Você acha que ganhou, mas o cache tá errado. Usuário muda nome, você não atualiza o cache. Ele vê nome velho por horas. Coloca TTL de 1 hora, achando que resolve. 100 mil usuários × 1 hora = você gerou 100 mil queries quando TTL expirou (thundering herd / cache stampede).

Realidade: cache é fácil de entender, difícil de fazer direito.

Cache-aside: o padrão mais comum

Você lê de Redis. Se não tem (miss), lê do banco e salva em Redis:

import redis
import json

cache = redis.Redis(host='localhost', port=6379)

def get_user_profile(user_id):
    # Passo 1: tentar cache
    key = f"user:{user_id}:profile"
    cached = cache.get(key)
    
    if cached:
        print(f"Cache hit")
        return json.loads(cached)
    
    # Passo 2: cache miss, consulta banco
    print(f"Cache miss, querying DB")
    user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
    
    # Passo 3: salva em Redis (com TTL)
    cache.setex(key, 3600, json.dumps(user))  # 3600 segundos = 1 hora
    
    return user

# Teste
user = get_user_profile(123)  # Cache miss (0 ms DB + 1 ms Redis) = 81 ms
user = get_user_profile(123)  # Cache hit (1 ms Redis) = 1 ms

Vantagem: simples, não precisa invalidar — TTL expira sozinho. Desvantagem: miss é caro (vai pro banco). Se 1 milhão de usuários acessam o mesmo perfil ao mesmo tempo e TTL expirou, 1 milhão de queries = cache stampede.

Write-through: garantir consistência

Quando salva, escreve em cache E no banco:

def update_user_profile(user_id, new_data):
    # Passo 1: atualiza banco
    db.query(f"UPDATE users SET name = '{new_data['name']}' WHERE id = {user_id}")
    
    # Passo 2: atualiza cache
    key = f"user:{user_id}:profile"
    cache.setex(key, 3600, json.dumps(new_data))
    
    return new_data

# Teste
update_user_profile(123, {'name': 'João Silva'})  # Atualiza DB e cache
user = get_user_profile(123)  # Cache hit, novo nome

Vantagem: cache sempre consistente com banco. Desvantagem: lento — duas operações (DB + Redis) são seriais. Se uma falha, está inconsistente.

Write-behind: rapido mas arriscado

Escreve em cache primeiro, depois no banco de forma assíncrona:

import asyncio

def update_user_profile_async(user_id, new_data):
    # Passo 1: salva em cache (rápido)
    key = f"user:{user_id}:profile"
    cache.setex(key, 3600, json.dumps(new_data))
    
    # Passo 2: agenda update do banco (assíncrono)
    asyncio.create_task(update_db_async(user_id, new_data))
    
    return {"status": "queued"}

async def update_db_async(user_id, new_data):
    # Simula delay de rede
    await asyncio.sleep(0.1)
    db.query(f"UPDATE users SET name = '{new_data['name']}' WHERE id = {user_id}")

# Teste
update_user_profile_async(123, {'name': 'Maria'})  # Retorna imediato (5 ms)

Vantagem: velocidade — retorna imediatamente. Desvantagem: risco — se aplicação crasha antes de sincronizar, perda de dados. Use com fila durável.

TTL e a escolha do tempo

Quanto tempo cachear?

# ❌ TTL muito longo
cache.setex(key, 86400, data)  # 24 horas — usuário vê dados de ontem

# ✓ TTL apropriado
cache.setex(key, 300, data)  # 5 minutos — bom balanço pra maioria

# ❌ TTL muito curto
cache.setex(key, 10, data)  # 10 segundos — expira rápido, cachê é inútil

Regra empírica:

  • Dados mutáveis frequentes (preço, estoque): 30 a 60 segundos
  • Dados estáveis (perfil, foto): 1 a 24 horas
  • Dados imutáveis (config, referência): sem TTL (PERSIST) ou dias

Invalidação: o problema difícil

Você não quer esperar TTL expirar. Quando atualiza, deleta cache:

def update_user_email(user_id, new_email):
    # Atualiza banco
    db.query(f"UPDATE users SET email = '{new_email}' WHERE id = {user_id}")
    
    # ❌ PROBLEMA: qual chave deletar?
    # cache.delete(f"user:{user_id}:profile")  # deleta perfil
    # cache.delete(f"user:{user_id}:stats")    # deleta stats
    # cache.delete(f"user:{user_id}:*")        # precisa SCAN
    
    # ✓ SOLUÇÃO 1: usar versão de cache (cache versioning)
    cache.incr(f"user:{user_id}:version")  # incrementa versão
    
    # Query agora precisa ler versão
    def get_user_profile(user_id):
        version = cache.get(f"user:{user_id}:version") or "1"
        key = f"user:{user_id}:profile:v{version}"
        # ...

Estratégia por tag (em código da aplicação):

# Salva com metadados
user_data = {
    'id': 123,
    'name': 'João',
    'email': 'joao@example.com'
}

cache.hset(f"user:123", mapping={
    'data': json.dumps(user_data),
    'version': '1',
    'tags': 'user-active,user-premium'  # tags separadas por vírgula
})

# Invalidação por tag
def invalidate_by_tag(tag):
    # Buscar todas as chaves com essa tag
    keys = cache.scan_iter(match=f"*")
    for key in keys:
        tags = cache.hget(key, 'tags')
        if tags and tag.encode() in tags:
            cache.delete(key)

Cache stampede: as 3 defesas

Problema: TTL expira, 1.000 requisições simultâneas, todas vão pro banco.

Defesa 1: Lock (Mutex)

Apenas 1 thread recomputa, outros esperam:

import threading

def get_user_with_lock(user_id):
    key = f"user:{user_id}:profile"
    
    # Passo 1: tentar cache
    cached = cache.get(key)
    if cached:
        return json.loads(cached)
    
    # Passo 2: cache miss, tomar lock distribuído
    lock_key = f"user:{user_id}:lock"
    lock = cache.lock(lock_key, timeout=2)  # lock por 2 segundos
    
    if lock.acquire(blocking=False):
        try:
            # Passo 3: verificar novamente (outro thread pode ter preenchido)
            cached = cache.get(key)
            if cached:
                return json.loads(cached)
            
            # Passo 4: computar (apenas este thread)
            user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
            cache.setex(key, 3600, json.dumps(user))
            return user
        finally:
            lock.release()
    else:
        # Outro thread pegou lock, espera que ele preencha cache
        time.sleep(0.1)
        cached = cache.get(key)
        return json.loads(cached)

Defesa 2: Jitter no TTL

Variar TTL evita expiração em massa:

import random

# ❌ Todos expiram no mesmo segundo
cache.setex(key, 3600, data)

# ✓ TTL com jitter (aleatório)
ttl = 3600 + random.randint(-300, 300)  # 3.600 ± 5 minutos
cache.setex(key, ttl, data)

# Resultado: expiram distribuídas ao longo de 10 minutos

Defesa 3: Recomputação antecipada

Antes de expirar, recomputa em background:

def get_user_smart(user_id):
    key = f"user:{user_id}:profile"
    ttl_key = f"user:{user_id}:ttl"
    
    cached = cache.get(key)
    if cached:
        # Verificar se tá perto de expirar
        ttl_remaining = cache.ttl(key)
        if ttl_remaining and ttl_remaining < 300:  # < 5 minutos
            # Recomputa em background (não bloqueia)
            asyncio.create_task(recompute_cache(user_id))
        
        return json.loads(cached)
    
    # Sem cache, computa normalmente
    user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
    cache.setex(key, 3600, json.dumps(user))
    return user

async def recompute_cache(user_id):
    # Thread separada recomputa
    user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
    cache.setex(f"user:{user_id}:profile", 3600, json.dumps(user))

O que NÃO cachear

# ❌ Passwords e tokens (nunca!)
cache.set(f"token:{user_id}", password)

# ❌ Dados com regras de conformidade (LGPD, GDPR)
# Pode violar direito ao esquecimento

# ❌ Valores computados rapidinho < 5 ms
# Cache custa 1 ms, não vale

# ✓ Queries que demoram > 50 ms
# ✓ Dados imutáveis ou raramente mutáveis
# ✓ Computações custosas (agregações, joins)

Medindo hit rate e custo

class MonitoredCache:
    def __init__(self, redis_conn):
        self.cache = redis_conn
        self.hits = 0
        self.misses = 0
    
    def get(self, key):
        value = self.cache.get(key)
        if value:
            self.hits += 1
        else:
            self.misses += 1
        return value
    
    def report(self):
        total = self.hits + self.misses
        hit_rate = (self.hits / total * 100) if total > 0 else 0
        print(f"Hit Rate: {hit_rate:.1f}% ({self.hits} hits, {self.misses} misses)")
        
        # Custo: cache miss = DB query (80 ms)
        # Cache hit = 1 ms
        time_saved = self.hits * (80 - 1) + self.misses * 80
        print(f"Tempo economizado: {time_saved / 1000:.1f}s")

cache = MonitoredCache(redis.Redis())
# Usar cache normalmente
cache.report()
# Hit Rate: 92.3% (923 hits, 77 misses)
# Tempo economizado: 72.5s

Quando NÃO usar Redis

Volume pequeno: se DB queries demoram 5 ms, Redis (1 ms) economiza pouco. Gasto com operação é maior que ganho.

Dados muito mutáveis: conta bancária muda a cada segundo. Cache inconsistente = bugs graves. Não vale a complexidade.

Infraestrutura sem DevOps: Redis precisa monitorar, backup, failover. Managed Redis (AWS ElastiCache) é mais simples, mas caro.

Armadilhas comuns

1. Evicção de dados por falta de memória:

WARNING: Memory used by Redis is above maxmemory policy

Configure limite e política (LRU vs LFU vs random):

# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru  # remove chaves menos usadas

2. Sem replicação/backup: Redis tá em memória. Falha, perde tudo. Sempre use replicação:

# docker-compose.yml
redis-primary:
  image: redis:7
  command: redis-server --port 6379

redis-replica:
  image: redis:7
  command: redis-server --port 6380 --slaveof redis-primary 6379

Próximos passos

Combine cache com observabilidade: Observabilidade em aplicações com LLM — monitora hit rate em tempo real.

Deploy Redis em produção com infra como código: Terraform na prática: sua primeira infra versionada.

Teste: cria Redis local (Docker), escreve app que faz GET/SET com monitoramento, mede hit rate com diferentes TTLs. Você vai ver empiricamente qual TTL melhor serve sua carga.

Aprenda jogando

Pacote Veloz

Atravesse a internet como um pacote de dados, desviando de latência e firewalls.

Jogar Pacote Veloz

Continue lendo