Neste artigo você vai montar uma camada de cache com Redis em Python, escolher entre cache-aside, write-through e write-behind, definir TTL sem sustos e evitar o cache stampede, aquele pico de queries que derruba o banco quando muitas chaves expiram juntas. Tudo vale também para o Valkey, o fork de código aberto do Redis, que fala o mesmo protocolo e usa o mesmo cliente.
O problema real
Um perfil de usuário leva algumas dezenas de milissegundos para sair do banco. Você põe um cache na frente e a leitura vira questão de um ou dois milissegundos. Ganho óbvio, mas surgem os problemas.
Primeiro, o usuário troca o nome e continua vendo o antigo, porque o cache não foi atualizado. Depois, você coloca TTL de uma hora, e quando milhares de chaves expiram no mesmo instante todas as requisições vão ao banco de uma vez. Cache é fácil de ligar e difícil de acertar.
Os exemplos abaixo usam redis-py (pip install redis) e uma função buscar_usuario_no_banco que você implementa com consultas parametrizadas, nunca montando SQL com f-string.
Cache-aside: o padrão mais comum
A aplicação pergunta ao cache primeiro. Se não achar (miss), busca no banco e guarda o resultado.
import json
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
TTL_PERFIL = 300 # segundos
def obter_perfil(user_id: int) -> dict:
chave = f"user:{user_id}:perfil"
em_cache = r.get(chave)
if em_cache is not None:
return json.loads(em_cache)
perfil = buscar_usuario_no_banco(user_id) # sua consulta parametrizada
r.set(chave, json.dumps(perfil), ex=TTL_PERFIL)
return perfil
Quando a aplicação atualiza o dado, ela apaga a chave, e a próxima leitura repovoa:
def atualizar_nome(user_id: int, novo_nome: str) -> None:
atualizar_nome_no_banco(user_id, novo_nome)
r.delete(f"user:{user_id}:perfil")
Apagar costuma ser mais seguro que reescrever o valor, porque evita gravar no cache algo que já está desatualizado numa corrida entre duas requisições. A desvantagem é que o miss custa caro, e é aí que nasce o stampede, tratado mais abaixo.
Write-through e write-behind
No write-through, toda escrita atualiza o banco e depois o cache, na mesma operação lógica. O cache fica coerente, mas a escrita demora mais e, se o segundo passo falhar, os dois divergem. Trate a falha do cache como não crítica: se não conseguir atualizar, apague a chave.
No write-behind, a escrita vai primeiro para o cache e o banco é atualizado depois, de forma assíncrona. É rápido, mas se o processo cair antes de sincronizar, o dado se perde. Só use com uma fila durável no meio (veja filas e workers) e para dados que toleram esse risco, como contadores de visualização. Para dados que o negócio não pode perder, fique com cache-aside ou write-through.
Escolhendo o TTL
O TTL é a rede de segurança: mesmo que você esqueça uma invalidação, o dado velho some sozinho. Escolha pelo custo de servir um dado desatualizado.
- Preço, estoque e saldo mudam rápido e o erro dói. Use segundos, ou invalide ativamente.
- Perfil, preferências e catálogo mudam pouco. Minutos a horas costumam servir.
- Configuração e tabelas de referência quase não mudam. Dias, ou sem TTL com invalidação no deploy.
Um TTL curto demais deixa o cache inútil (quase tudo é miss), e um longo demais exige invalidação perfeita. Comece conservador e ajuste olhando a taxa de acerto.
Invalidação de várias chaves
O difícil é saber quais chaves apagar quando um dado muda. Evite KEYS user:*, que varre o banco inteiro e trava o servidor. SCAN é seguro para manutenção, mas lento para usar a cada escrita.
Uma técnica simples é versionar a chave. Guarde um número de versão por usuário e inclua na chave dos itens derivados:
def chave_versionada(user_id: int, nome: str) -> str:
versao = r.get(f"user:{user_id}:v") or "0"
return f"user:{user_id}:v{versao}:{nome}"
def invalidar_usuario(user_id: int) -> None:
r.incr(f"user:{user_id}:v") # as chaves antigas ficam órfãs e expiram pelo TTL
Ela troca espaço por simplicidade: as chaves antigas continuam ocupando memória até o TTL vencer, então sempre use TTL junto.
Outra opção é guardar, num SET do Redis, a lista de chaves que dependem de uma entidade, e apagar todas pelo SET quando ela muda.
Cache stampede: três defesas
O stampede acontece quando uma chave quente expira e centenas de requisições fazem miss ao mesmo tempo, todas indo ao banco.
Defesa 1: lock para um só recalcular
Só quem pega o lock vai ao banco. Os outros esperam um instante e leem o resultado.
import time
def obter_com_lock(user_id: int) -> dict:
chave = f"user:{user_id}:perfil"
for _ in range(20):
em_cache = r.get(chave)
if em_cache is not None:
return json.loads(em_cache)
# SET NX EX: só um processo consegue criar a chave do lock
if r.set(f"lock:{chave}", "1", nx=True, ex=5):
try:
perfil = buscar_usuario_no_banco(user_id)
r.set(chave, json.dumps(perfil), ex=TTL_PERFIL)
return perfil
finally:
r.delete(f"lock:{chave}")
time.sleep(0.05) # outro processo está recalculando
return buscar_usuario_no_banco(user_id) # último recurso
Esse lock simples serve para proteger o banco de rajadas. Ele não oferece as garantias de um lock distribuído forte, e para esse uso não precisa.
Defesa 2: jitter no TTL
Se todas as chaves nascem juntas com o mesmo TTL, elas morrem juntas. Espalhe a expiração:
import random
def ttl_com_jitter(base: int, fracao: float = 0.1) -> int:
return int(base * random.uniform(1 - fracao, 1 + fracao))
r.set(chave, valor, ex=ttl_com_jitter(3600))
Defesa 3: servir o dado velho e atualizar em segundo plano
Guarde junto do valor um prazo "macio". Quando ele passa, a requisição devolve o dado atual do cache e dispara a atualização, sem esperar. Quem implementa isso costuma combinar com o lock da defesa 1, para que só uma atualização rode por vez. É o padrão conhecido como stale-while-revalidate.
Medindo a taxa de acerto
Não precisa de contador na aplicação. O próprio Redis conta:
redis-cli INFO stats | grep -E "keyspace_(hits|misses)"
A taxa de acerto é hits / (hits + misses). Acompanhe ao longo do tempo, não num instante. Se está baixa, o TTL pode estar curto demais, as chaves podem ser específicas demais, ou você está cacheando algo que quase ninguém lê repetido. Em produção, exporte essas métricas para o Prometheus, como no artigo de monitoramento com Prometheus e Grafana.
O que não cachear
- Senhas, tokens de acesso e segredos em geral.
- Dados pessoais sem pensar na LGPD: se o titular pede exclusão, o cache também precisa esquecer. Defina TTL e saiba onde as cópias estão.
- Resultados que o banco já devolve em poucos milissegundos e que raramente se repetem.
- Dados que mudam a cada instante e exigem consistência forte, como o saldo exato de uma conta.
Configuração de memória e persistência
Sem limite de memória, o Redis cresce até o sistema matar o processo. Defina um teto e uma política de remoção:
maxmemory 2gb
maxmemory-policy allkeys-lru
allkeys-lru remove as chaves menos usadas recentemente quando enche, o que é o comportamento esperado de um cache. Se o mesmo Redis também guarda filas ou dados que não podem sumir, use uma instância separada, porque essa política apagaria esses dados também.
Cache pode ser perdido, então réplica e persistência são opcionais para ele. Se você quer reduzir o impacto de um reinício frio, uma réplica ajuda. Em Compose:
services:
redis-primary:
image: redis:8
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
redis-replica:
image: redis:8
command: redis-server --port 6380 --replicaof redis-primary 6379
A flag --replicaof substituiu a antiga --slaveof. Em produção, prefira um serviço gerenciado ou o Redis Sentinel para failover automático.
Quando não usar cache
Se a consulta já responde em poucos milissegundos, o cache adiciona complexidade e um novo ponto de falha para ganhar pouco. Antes de cachear, procure o motivo da lentidão, que muitas vezes é um índice faltando, como mostra o artigo sobre Postgres para desenvolvedores.
Próximos passos
Monte um Redis local com Docker, escreva a função obter_perfil com uma consulta simulada que dorme 50 ms, e dispare requisições concorrentes com e sem o lock para ver o stampede acontecendo. Depois confira a taxa de acerto no INFO stats.
Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria