Pular para o conteúdo
CloudIntermediário

Cache com Redis na prática: padrões, TTL e stampede

Implemente cache com Redis do jeito certo: cache-aside, invalidação, TTL com jitter e proteção contra cache stampede, com código Python que roda.

Por Equipe IAUAI Estudos · 5 de agosto de 2026 · 9 min de leitura

Nesta página

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

Aprenda jogando

Pacote Veloz

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

Jogar Pacote Veloz

Teste seu conhecimento

Teste o que você aprendeu sobre cache com Redis

Pergunta 1 de 6

No cache-aside, o que a aplicação faz ao atualizar um dado?

Um conteúdo prático por quinzena

Deixe o seu e-mail para receber um conteúdo prático de IA e cloud a cada quinzena. Sem spam, e você pede a exclusão quando quiser.

Continue lendo