01 - Cache de Aplicação (Redis, Memcached)
video: https://youtu.be/... no frontmatter da nota.Cache de Aplicação (Redis, Memcached)
Cache de aplicação distribuído é uma camada de armazenamento em memória, compartilhada entre todas as instâncias da aplicação, que guarda resultados reutilizáveis para evitar recomputá-los ou reconsultá-los. Redis e Memcached são os dois protagonistas desse papel, e a existência deles como infraestrutura separada, e não biblioteca embutida, é o que transforma cache de otimização local em capacidade de arquitetura: qualquer réplica da aplicação encontra o mesmo dado quente, o custo de um miss é pago uma vez e o ganho é sentido pelo serviço inteiro. Em produção, o cache é geralmente o primeiro ajuste de performance com retorno imediato, reduzindo latência p95, descarregando o banco de dados e absorvendo picos de leitura que derrubariam o primário. O preço é uma classe nova de problemas: dados obsoletos, validade (stale), stampede de cache frio e a disciplina de invalidação. Este arquivo cobre o mecanismo, os trade-offs entre local e distribuído, os detalhes de Redis e Memcached em cluster e os casos onde o cache não ajuda.
Conceito e cache local versus distribuído
Cache funciona pelo princípio da localidade temporal: dados consultados agora tendem a ser consultados de novo em janela curta, e guardar o resultado em memória custa pouco e evita o caminho caro (consulta SQL, chamada remota, computação pesada). O hit é a leitura que encontra o dado no cache; o miss é a que não encontra e precisa pagar o caminho completo antes de popular o cache.
O cache local (em memória do processo, com bibliotecas como Caffeine ou o cache in-process) é o mais rápido que existe, nano ou microssegundos, porque não há rede. Seu problema é a incoerência: cada instância da aplicação tem sua cópia, invalidar todas simultaneamente é impossível, e o balanceamento de carga faz o mesmo usuário cair em instâncias com estados diferentes.
O cache distribuído resolve a coerência e o desperdício: um serviço central, acessado por rede, guarda uma única cópia lógica do dado para todo o fleet. A latência de rede de meio a alguns milissegundos ainda é ordens de magnitude menor que a consulta SQL típica, e o custo de memória é consolidado, sem duplicação por instância.
| Aspecto | Cache local (in-process) | Cache distribuído (Redis, Memcached) |
|---|---|---|
| Latência de leitura | Nanossegundos a microssegundos | Fração de milissegundo a poucos ms |
| Coerência entre instâncias | Nenhuma, cada processo tem sua cópia | Única cópia lógica compartilhada |
| Capacidade | Limitada pela heap/memória do pod | Escalável, nós dedicados independentes |
| Sobrevive a restart da app | Não, perde tudo a cada deploy | Sim, ciclo de vida separado |
| Caso típico | Dados quase estáticos, referência | Sessões, consultas, objetos computados |
A combinação de produção frequente usa os dois: cache local de referência estática (catálogos, flags, configuração) sobre cache distribuído de dados de negócio. A invalidação do local, o problema que ele sempre carrega, fica tolerável quando o conteúdo local muda em deploy, não em runtime.
A economia do cache é medida pelo hit ratio e pelo custo relativo dos caminhos. Se 90% das leituras são hits em 1 ms e 10% são misses que pagam 100 ms de SQL, a latência média é de aproximadamente 10.9 ms, uma melhoria de quase 10 vezes sobre os 100 ms sem cache. A mesma conta com hit ratio de 50% cai para 50.5 ms, e o gráfico da latência média em função do hit ratio é hipérbole: os ganhos maiores estão nos últimos pontos percentuais, exatamente por isso que invalidação e stampede merecem engenharia dedicada.
Redis e Memcached em cluster
Memcached é um cache puro: hash map distribuído em memória, com chaves e valores binários até 1 MB, sem persistência, sem estruturas de dados e sem replicação nativa. O modelo de cluster é client-side sharding: a biblioteca da aplicação calcula, via hash consistente, em qual nó cada chave vive, e adicionar ou remover nós redistribui as chaves com o mínimo de churn que o hash consistente consegue garantir. A ausência de failover é deliberada: se um nó morre, ele é removido do anel e o cache se reconstitui pela carga natural, aceitando o pico de misses.
Redis é um servidor de estruturas de dados: strings, hashes, listas, sets, sorted sets, streams, com TTL por chave, transações simples, pub/sub e scripts Lua atômicos. O clustering oficial (Redis Cluster) é server-side sharding com 16384 slots de hash: cada nó primário dono de um intervalo de slots, réplicas para leitura e failover, e o cliente recebe o mapa de slots e redireciona requisições com MOVED/ASK quando a topologia muda.
A escolha entre os dois segue a natureza do dado. Para cache puro de chave-valor com paralelismo extremo e simplicidade total, Memcached é eficiente e previsível. Para sessões, ranking, filas leves, contadores, idempotência por chave e qualquer lógica que se beneficie de estruturas ou de scripts atômicos, Redis é o caminho. A dominância atual de Redis (e do fork Valkey) vem dessa versatilidade.
O dimensionamento de cluster nasce da aritmética da memória: tamanho do working set quente, fator de crescimento e folga para failover definem o número de nós. A regra prática é provisionar memória total com 30 a 50% de folga sobre o pico observado de maxmemory, porque eviction agressiva em pico degrada o hit ratio exatamente quando ele importa. No Redis Cluster, a distribuição das chaves por slot é uniforme para chaves bem distribuídas, e manter chaves relacionadas no mesmo slot (hash tags com chaves entre chaves, como {pedido:42}:itens) habilita operações multi-key atômicas ao custo de risco de concentrar carga, decisão que merece consciência de trade-off.
# Redis: padrões básicos de uso em cache de aplicação
# Grava o resultado serializado de uma consulta com TTL de 5 minutos
SET pedidos:resumo:cliente:42 '{"total":12,"valor":89.90}' EX 300
# Leitura com miss explícito (nil sinaliza popular o cache)
GET pedidos:resumo:cliente:42
# Hash para objetos com campos, evita serializar o objeto inteiro
HSET sessao:abc usuario 42 etapa "checkout" expira_em "15m"
EXPIRE sessao:abc 900
# Operação atômica de contador, casos como rate limit e métrica
INCR rate:login:ip:10.0.0.8
EXPIRE rate:login:ip:10.0.0.8 60
# Diagnóstico: taxa de hits, chaves e memória do nó
INFO stats # keyspace_hits / keyspace_misses = hit ratio
INFO memory # used_memory, mem_fragmentation_ratio
TTL e gerenciamento de memória
O TTL (time to live) é o controle primário de validade: cada chave carrega seu prazo, e o nó expira chaves por amostragem ativa (varre aleatoriamente chaves com prazo vencido) e de forma lazy (valida na leitura). Um TTL não é garantia de deleção exata no segundo marcado, é uma janela, e o desenho do sistema nunca deve assumir expiração pontual como semântica de negócio.
A escolha do TTL é a decisão de produção mais recorrente do cache. Valores curtos reduzem a janela de dado obsoleto e aumentam a carga no banco; valores longos maximizam o hit ratio e ampliam a janela de stale. O ponto de partida típico é minutos (1 a 15) para dados de negócio e horas para referência estática, com ajuste guiado pela tolerância real de obsolescência de cada dado, e não por um valor global único.
Memória cheia dispara a política de eviction. O padrão do Redis é noeviction (rejeita escritas), e a configuração de produção usual é allkeys-lru (descarta as chaves menos usadas) ou volatile-lru (descarta apenas entre as que têm TTL). Memcached usa LRU por padrão sem configuração visível. Definir maxmemory com folga em relação à memória do nó e escolher a política explicitamente evita os dois desastres opostos: escritas falhando e OOM kill.
Fragmentação e hot keys são os detalhes que quebram a teoria. Valores grandes e ciclos de write-delete fragmentam a memória (mem_fragmentation_ratio acima de 1.5 pede investigação), e uma única chave muito acessada concentra tráfego em um nó do cluster, o hot key, que precisa de réplicas locais de leitura, prefixo com salt ou decomposição do dado.
O jitter de TTL é o microdetalhe de maior retorno na operação diária. Milhares de chaves criadas no mesmo segundo com TTL idêntico expiram em bloco, produzindo ondas periódicas de miss que atingem o banco em sincronia; adicionar aleatoriedade de 10 a 20% sobre o TTL base (300 segundos vira 270 a 330) espalha as expirações no tempo e elimina a onda sem custo nem mudança de semântica.
Serialização e o custo por chave
Tudo que entra no cache serializa: objeto vira bytes na escrita e volta na leitura, e a escolha do formato pesa em latência, memória e compatibilidade. JSON é legível e universal, mas verboso; formatos binários compactos (MessagePack, Protobuf) reduzem memória e custo de rede; o formato nativo do framework é cômodo e cria acoplamento com a versão da aplicação.
A prática de produção fixa três regras de serialização. Primeira: versionar o formato da chave ou do payload, evitando que o deploy novo leia objetos com schema antigo e quebre; prefixo v2: na chave resolve a maioria dos casos. Segunda: serializar o mínimo necessário, campos grandes e não usados ficam fora. Terceira: definir limite superior de tamanho por valor, valores de megabytes dominam a latência do cache e pedem compressão ou decomposição.
Chaves precisam de convenção de namespace desde o primeiro dia. O padrão dominante é serviço:dominio:identificador, como catalogo:produto:123, que permite purga seletiva por padrão, diagnóstico de ocupação por domínio e evita colisões entre aplicações compartilhando o cluster.
Aplicações práticas: consultas, sessões e objetos computados
Os três usos dominantes do cache de aplicação cobrem perfis de dado diferentes, e misturá-los no mesmo namespace sem convenção é origem de quase todo incidente de invalidação.
- Cache de consultas: resultado de queries caras, agregações, listagens de página inicial e respostas de API de terceiros; chave derivada dos parâmetros da consulta, TTL de minutos, invalidação por evento nas escritas relevantes.
- Cache de sessão: estado de autenticação e preferências do usuário; leitura em cada requisição, TTL de renovação deslizante (EXPIRE a cada acesso) e atenção ao logout, que exige deleção explícita, não espera por expiração.
- Cache de objetos computados: resultado de computação cara como score, ranking e enriquecimento; a chave deve incluir a versão do algoritmo (score:v3:cliente:42) para que o deploy novo não leia computações da versão antiga.
O exemplo a seguir mostra o padrão canônico de leitura com fallback, a espinha dorsal de qualquer uso de cache em aplicação.
# Cache-aside canônico: leitura com fallback ao banco
import json, redis
r = redis.Redis(host="cache.interno", decode_responses=True)
def obter_produto(pid: int):
chave = f"catalogo:produto:v2:{pid}" # namespace e versão no formato
bruto = r.get(chave)
if bruto is not None: # hit: caminho barato
return json.loads(bruto)
produto = db.buscar_produto(pid) # miss: caminho caro
r.set(chave, json.dumps(produto), ex=300) # popula com TTL de 5 minutos
return produto
A observabilidade mínima do cache em produção fecha o ciclo de operação. Hit ratio por namespace, uso de memória contra maxmemory, chaves evitadas por segundo, latência p99 do acesso e tamanho médio de valor formam o painel; com esses cinco números, decisões de TTL, dimensionamento e refatoração de chaves deixam de ser palpite e viram resposta a dado medido.
Quando cache não ajuda
Cache é aceleração para leituras repetidas e repetíveis, e reconhecer quando o dado não tem esse perfil evita gastar memória sem ganho, ou pior, introduzir incoerência sem benefício.
- Dado escrito e lido no mesmo ciclo: o valor nunca é reusado, e cada escrita gera custo de cache sem hit nenhum.
- Tamanho de working set maior que a memória disponível: se o conjunto quente não cabe, o hit ratio despenca e a latência fica pior pelo desvio extra na leitura.
- Forte exigência de frescor absoluto: saldo contábil disponível para débito e estoque crítico de reserva exigem leitura da fonte com transação; cache aqui vira fonte de erro de negócio.
- Latência da fonte já menor que o custo do acesso: uma consulta indexada de 1 ms não justifica um round trip de rede de cache de 2 ms em p99.
- Acessos puramente aleatórios e singulares: sem localidade temporal, o hit ratio tende ao zero estatístico.
Erros comuns e boas práticas
- Usar TTL global para todo tipo de dado; cada domínio tem sua tolerância de obsolescência, e o TTL é decisão por tipo de chave, documentada junto com a convenção de namespace.
- Cache sem TTL nenhum: chaves eternas viram lixo acumulado e dado obsoleto permanente; qualquer SET precisa de EX, e auditoria de chaves sem TTL entra na rotina.
- Ignorar hit ratio como métrica de primeira classe: keyspace_hits e misses por namespace, com alerta de queda, é a telemetria mínima que separa cache vivo de cache decorativo.
- Serializar objetos sem versão: deploy novo lendo payload de schema antigo é bug intermitente difícil de rastrear; versão no formato ou na chave é barato e resolve.
- Deixar o banco pagar o stampede: chave quente expirando em horário de pico despeja todas as requisições no banco ao mesmo tempo; locking de população e TTL com jitter tratam o problema (detalhado no arquivo de invalidação).
- Confundir cache com armazenamento: dado sem o qual o negócio para de funcionar (assinatura, carrinho em faturamento) precisa de persistência real; cache é acelerador, com perda aceitável.
- Compartilhar um cluster entre ambientes: staging e produção no mesmo Redis trocam chaves, custo e incidentes; isolamento por cluster ou por prefixo rígido com ACL.
- Esquecer que o cache está no caminho de latência do usuário: uma instância de cache sob saturação de rede ou CPU vira o gargalo novo, e monitorar o próprio cache como dependência crítica, com circuit breaker na aplicação, é parte do desenho maduro.