Projetando um Cache Distribuído: Guia Completo de System Design
![]()
Resumo em português: Um guia completo de system design para cache distribuído com Redis e Memcached, cobrindo cache-aside, read-through, write-through, invalidação, TTL, eviction, sharding, replicação, hot keys, prevenção de dogpile, multi-região, segurança, observabilidade e decisões fortes para entrevistas.
Publicado: Fevereiro 2026
Tempo de leitura: 90 minutos
Palavras-chave: #SystemDesign #CacheDistribuido #Redis #Memcached #Caching #Escalabilidade #Confiabilidade #EntrevistaTécnica
Em escala pequena, cache parece atalho.
Em produção, cache vira sistema distribuído.
Ele tem pressão de memória, regras de roteamento, dados stale, comportamento de cliente, falhas parciais, segurança, custo e operação.
O erro comum é tratar Redis ou Memcached como camada mágica de velocidade.
O modelo mental correto é mais rígido:
- banco de dados guarda a verdade durável,
- cache guarda aceleração temporária,
- clientes precisam de fallback cuidadoso,
- plataforma precisa controlar invalidação, capacidade e visibilidade.
Cache distribuído bom reduz latência e carga no banco sem virar fonte oculta de verdade.
Este artigo desenha esse sistema de ponta a ponta.
Redis e Memcached aparecem juntos porque ambos continuam relevantes.
Redis oferece estruturas ricas, replicação, persistência opcional, Lua/functions, streams, sorted sets e Redis Cluster.
Memcached oferece um cache key-value em memória simples, rápido e normalmente shardado no cliente.
Nenhum deles elimina design.
Sumário
- Análise de Requisitos
- Cálculos de Envelope
- Arquitetura de Alto Nível
- Design de API
- Modelagem de Dados
- Padrões Centrais de Cache
- Sharding e Roteamento
- Hash Slots no Redis Cluster
- Memcached em Escala
- Replicação e Failover
- Cache Multi-Região
- Invalidação e Estratégia de TTL
- Eviction e Gestão de Memória
- Semântica de Consistência
- Mitigação de Hot Keys
- Thundering Herd e Prevenção de Dogpile
- Negative Caching e Admission Control
- Confiabilidade e Degradação
- Segurança
- Observabilidade e SLOs
- Dicas de Entrevista
- Anti-Patterns
- Conclusão
- Referências
- Referência Rápida
Análise de Requisitos
A primeira decisão em entrevista é escopo.
Não comece por Redis.
Comece pelo workload.
Requisitos Funcionais
- Servir objetos lidos com frequência com latência menor que a fonte de verdade.
- Reduzir pressão de leitura em bancos primários e APIs downstream.
- Suportar leituras e escritas key-value para objetos serializados.
- Suportar expiração por TTL.
- Suportar invalidação explícita depois de writes.
- Suportar fallback seguro em cache miss ou falha do cache.
- Suportar escala horizontal entre nós de cache.
- Suportar chaves de alta cardinalidade sem posicionamento manual.
- Suportar mitigação de hot keys.
- Expor métricas de hit rate, latência, memória, evictions e erros.
- Suportar controle de acesso e transporte criptografado quando necessário.
- Manter comportamento previsível durante deploys, failovers e resharding.
Requisitos Não Funcionais
| Requisito | Meta | Por que importa |
|---|---|---|
| Latência de get | < 2ms p99 dentro da região | cache precisa ganhar do banco |
| Disponibilidade do cache | 99,99% no caminho de leitura | falha não pode derrubar app |
| Hit rate | 80-95% para objetos cacheáveis | abaixo disso custo pode não fechar |
| Tolerância a stale | explícita por objeto | cada dado envelhece de forma diferente |
| Eficiência de memória | medida por classe de objeto | memória é custo principal |
| Tempo de failover | segundos a poucos minutos | incidente longo sobrecarrega banco |
| Estabilidade de roteamento | pouco movimento em mudança de nó | resharding não deve esfriar tudo |
| Clareza operacional | alertas ligados a impacto | hit rate sozinho engana |
Perguntas de Clarificação
- Quais dados podem ser cacheados?
- Quanto stale cada objeto pode ficar?
- Cache é local a um serviço ou compartilhado por vários serviços?
- Read-through mora numa biblioteca, proxy ou aplicação?
- Qual distribuição de tamanho dos objetos?
- Qual QPS de pico e qual QPS de miss?
- O que acontece se o cache cair?
- Consistência cross-region é obrigatória?
- Dados exigem criptografia ou isolamento forte?
- Writes são frequentes o suficiente para complicar invalidação?
Premissas Deste Design
Este desenho assume:
- produto SaaS multi-tenant,
- workload read-heavy,
- deploy regional de aplicações,
- Redis Cluster para casos com recursos ricos,
- pool Memcached para cache simples de objetos,
- banco relacional como fonte de verdade,
- Kafka ou equivalente para eventos de invalidação,
- meta de p99 abaixo de 150ms em APIs de usuário,
- budget de staleness de 30 segundos para perfis e catálogo,
- política de não cachear decisões críticas de autorização sem versionamento explícito.
O Que Este Cache Não É
Cache não é banco primário.
Cache não é log de auditoria.
Cache não é ledger de pagamento.
Cache não é a única cópia de estado de usuário.
Cache pode esquecer.
Esquecer é recurso quando a fonte de verdade continua correta.
Esquecer é incidente quando o cache virou banco sem ninguém admitir.
Cálculos de Envelope
Declare premissas antes de números.
O número exato importa menos que o formato da pressão.
Escala Assumida
Usuários ativos mensais: 80 milhões
Usuários ativos diários: 20 milhões
Requests de API no pico: 1.200.000/s
Requests médios de API: 250.000/s
Percentual de leitura cacheável: 65%
Hit rate alvo: 90%
Tamanho médio de objeto cacheado: 2 KB
P95 de objeto cacheado: 12 KB
Objetos quentes possíveis: 250 milhões de chaves
Objetos alterados por dia: 40 milhões
Regiões: 3 ativas
Carga de Requests
Leituras cacheáveis no pico = 1.200.000 * 0,65
= 780.000 leituras/s
Com hit rate de 90%:
cache hits/s = 702.000
cache misses/s = 78.000
78.000 leituras por segundo no banco ainda é muito.
O caminho de miss precisa de proteção.
O cache deve reduzir carga média e limitar amplificação em miss.
Estimativa de Memória
Objetos quentes lógicos: 250.000.000
Payload médio: 2 KB
Overhead de metadados e allocator: 35%
Fator de replicação: 2
Payload bruto = 250M * 2KB
~= 500 GB
Com overhead = 500GB * 1,35
~= 675 GB
Com uma réplica = 675GB * 2
~= 1,35 TB
Essa é estimativa direcional.
Memória real no Redis muda com encoding, tamanho de chave, tipo de objeto, fragmentação, buffers de replicação e buffers de persistência.
Memória real no Memcached depende de slab classes, distribuição de tamanho e espaço desperdiçado em chunks.
Estimativa de Rede
Banda de cache hit no pico ~= 702.000 * 2KB
~= 1,4 GB/s de payload
Com overhead de protocolo, TLS e objetos p95:
planeje múltiplos GB/s dentro da região.
Rede não é detalhe.
Cache pode mover gargalo de CPU do banco para NICs, conexões de cliente ou custo cross-AZ.
Estimativa de Miss Storm
Se deploy limpa 30% das chaves quentes no pico:
novo miss rate = misses normais + chaves frias
miss QPS normal = 78.000
leitura fria extra = 780.000 * 0,30
= 234.000
miss QPS total = 312.000
Se cada miss abre 3 queries:
QPS no banco = 936.000
Isso pode derrubar a fonte de verdade.
Warming, request coalescing, TTL com jitter e admission control não são opcionais em escala alta.
Insight Central
Design de cache distribuído é design para limitar dano em miss.
O caminho feliz é simples.
O caminho de miss decide se o sistema sobrevive.
Arquitetura de Alto Nível
flowchart TB
subgraph Clientes
WEB["Web / Mobile"]
API_CLIENT["Clientes de API"]
WORKERS["Workers assíncronos"]
end
subgraph Aplicacao["Camada de Aplicação"]
EDGE["Edge / Gateway"]
SVC["Serviços"]
CACHE_LIB["Biblioteca de Cache"]
COALESCE["Request Coalescer"]
end
subgraph CacheLayer["Camada de Cache"]
ROUTER["Client Router / Proxy"]
REDIS[("Redis Cluster")]
MEMCACHED[("Pool Memcached")]
LOCAL["Cache Local em Processo"]
end
subgraph Verdade["Fonte de Verdade"]
DB[("Banco Primário")]
SEARCH[("Índice de Busca")]
OBJECTS[("Object Store")]
end
subgraph Invalidacao["Invalidação"]
CDC["Change Data Capture"]
BUS[("Event Bus")]
INVALIDATOR["Workers de Invalidação"]
end
subgraph Observabilidade
METRICS["Métricas"]
LOGS["Logs estruturados"]
TRACES["Traces"]
ALERTS["Alertas"]
end
WEB --> EDGE
API_CLIENT --> EDGE
EDGE --> SVC
WORKERS --> SVC
SVC --> CACHE_LIB
CACHE_LIB --> LOCAL
CACHE_LIB --> COALESCE
COALESCE --> ROUTER
ROUTER --> REDIS
ROUTER --> MEMCACHED
SVC --> DB
SVC --> SEARCH
SVC --> OBJECTS
DB --> CDC
CDC --> BUS
BUS --> INVALIDATOR
INVALIDATOR --> REDIS
INVALIDATOR --> MEMCACHED
CACHE_LIB --> METRICS
ROUTER --> METRICS
REDIS --> METRICS
MEMCACHED --> METRICS
SVC --> LOGS
SVC --> TRACES
METRICS --> ALERTS
Princípios de Arquitetura
- Mantenha leitura correta sem cache.
- Deixe uso de cache explícito no limite do serviço.
- Centralize construção de chaves em uma biblioteca.
- Use cache local só para objetos pequenos, seguros e curtos.
- Use cache distribuído para objetos quentes compartilhados.
- Use invalidação por evento para famílias com muita escrita.
- Use TTL como rede de segurança, não como única correção.
- Proteja misses com coalescing e backpressure.
- Trate hot key como problema central de capacidade.
- Meça frescor, não só hit rate.
Escolha de Cache por Workload
| Workload | Melhor encaixe | Observação |
|---|---|---|
| objetos serializados simples | Memcached | rápido, simples, shardado no cliente |
| contadores e estado atômico | Redis | operações atômicas e scripts |
| feeds ordenados ou rankings | Redis | sorted sets ajudam, custo de memória alto |
| sessões ou estado efêmero | Redis ou Memcached | depende de durabilidade esperada |
| decisões de autorização | geralmente evitar | use versões e TTL curto quando necessário |
| read models caros | Redis ou Memcached | combine com eventos de invalidação |
| blobs grandes | geralmente evitar | guarde ponteiro ou resumo comprimido |
Design de API
API de cache distribuído tem duas camadas:
- abstração usada pela aplicação,
- operações usadas pela infraestrutura.
A aplicação não deveria conhecer todo comando Redis.
A plataforma não deveria esconder semânticas como TTL, stale tolerance e negative caching.
Interface do Cliente
export type CacheKey = string;
export type CachePolicy = {
ttlSeconds: number;
staleWhileRevalidateSeconds?: number;
negativeTtlSeconds?: number;
jitterRatio?: number;
namespace: string;
version: string;
allowStaleOnError: boolean;
maxSerializedBytes: number;
};
export type CacheResult<T> =
| { status: "hit"; value: T; ageMs: number }
| { status: "miss" }
| { status: "stale"; value: T; ageMs: number; reason: "refreshing" | "origin_error" };
export interface DistributedCache {
get<T>(key: CacheKey, policy: CachePolicy): Promise<CacheResult<T>>;
set<T>(key: CacheKey, value: T, policy: CachePolicy): Promise<void>;
delete(key: CacheKey): Promise<void>;
getOrLoad<T>(
key: CacheKey,
policy: CachePolicy,
loader: () => Promise<T | null>
): Promise<T | null>;
}
Exemplo no Serviço
async function getProductPage(productId: string, viewerRegion: string) {
const key = cacheKeys.productPage(productId, viewerRegion);
return cache.getOrLoad(
key,
{
namespace: "catalog",
version: "v4",
ttlSeconds: 900,
staleWhileRevalidateSeconds: 60,
negativeTtlSeconds: 30,
jitterRatio: 0.15,
allowStaleOnError: true,
maxSerializedBytes: 64 * 1024
},
async () => {
const product = await productRepository.findRenderableProduct(productId, viewerRegion);
return product ?? null;
}
);
}
Exemplos de Comandos Redis
Use comandos como vocabulário operacional.
Não espalhe comando cru por todo serviço.
SET catalog:v4:product:us-east-1:123 "{...json...}" EX 900
GET catalog:v4:product:us-east-1:123
DEL catalog:v4:product:us-east-1:123
MGET catalog:v4:product:us-east-1:123 catalog:v4:product:us-east-1:456
INCRBY metrics:v1:product:123:views 1
EXPIRE metrics:v1:product:123:views 3600
Exemplos de Comandos Memcached
set catalog:v4:product:us-east-1:123 0 900 128
{"id":"123","name":"Keyboard","price":12900}
get catalog:v4:product:us-east-1:123
delete catalog:v4:product:us-east-1:123
add lock:v1:product:123 0 10 1
1
Exemplo SQL da Fonte de Verdade
SELECT
p.id,
p.name,
p.price_cents,
p.status,
p.updated_at,
i.available_quantity
FROM products p
JOIN inventory i ON i.product_id = p.id
WHERE p.id = $1
AND p.status = 'active';
Modelagem de Dados
Modelagem de cache começa por chave.
Chave ruim cria stale data, vazamento cross-tenant, partição quente e migração dolorosa.
Anatomia da Chave
<namespace>:<schema-version>:<tenant-ou-regiao>:<entity>:<id>:<variant>
Exemplo:
catalog:v4:tenant_42:product:123:currency_usd
profile:v2:tenant_42:user:9001:public
permissions:v8:tenant_42:user:9001:resource:invoice_77
negative:v1:tenant_42:product:missing_123
Regras de Chave
- Inclua tenant ou fronteira de isolamento.
- Inclua versão de schema.
- Inclua região quando a região muda resposta.
- Inclua segmento de viewer quando personalização muda resposta.
- Mantenha chaves curtas para reduzir gasto de memória.
- Evite strings livres vindas do usuário.
- Evite chaves que exigem scan global.
- Evite dados sensíveis em texto claro na chave.
- Prefira builders determinísticos a concatenação manual.
- Use versão para migração em vez de delete massivo quando possível.
Envelope do Valor Cacheado
type CacheEnvelope<T> = {
payload: T;
createdAtEpochMs: number;
sourceVersion: string;
entityUpdatedAtEpochMs?: number;
softTtlEpochMs?: number;
hardTtlEpochMs: number;
compression?: "none" | "zstd" | "gzip";
};
Envelope permite decidir frescor depois do get.
Também ajuda quando invalidação falha parcialmente.
Classes de Objeto
| Classe | TTL | Invalidação | Padrão | Observação |
|---|---|---|---|---|
| product page model | 15 min | evento após update | cache-aside | stale curto aceitável |
| perfil público | 10 min | evento após update | cache-aside | incluir versão de privacidade |
| feature flags | 30 s | push por stream | read-through/local | correção importa |
| permissões | 5-30 s | chave versionada | cache-aside | stale grant é risco |
| produto inexistente | 30 s | evento de create opcional | negative cache | evita misses repetidos |
| relatório caro | 1 h | delete explícito | write-through | observar tamanho |
Serialização
JSON é fácil de depurar.
MessagePack, Protobuf ou FlatBuffers reduzem payload e CPU em alguns casos.
Compressão ajuda valores grandes, mas custa CPU e latência.
Regra prática:
- não comprima objetos pequenos,
- avalie compressão acima de 4-8 KB,
- meça CPU antes de ligar globalmente,
- guarde codec no envelope,
- preserve compatibilidade em rolling deploy.
Padrões Centrais de Cache
Cache-Aside
Cache-aside é o padrão mais comum.
A aplicação controla miss.
sequenceDiagram
participant Client
participant Service
participant Cache
participant DB
Client->>Service: GET /products/123
Service->>Cache: GET product:123
alt cache hit
Cache-->>Service: value
Service-->>Client: response
else cache miss
Cache-->>Service: miss
Service->>DB: SELECT product
DB-->>Service: row
Service->>Cache: SET product:123 EX 900
Service-->>Client: response
end
Benefícios:
- simples,
- explícito,
- resiliente quando cache falha,
- funciona com Redis e Memcached,
- entra bem em sistemas existentes.
Custos:
- lógica de miss repetida,
- TTL fácil de esquecer,
- risco de stampede,
- stale até TTL ou invalidação.
Read-Through
Read-through move o carregamento para biblioteca ou camada de cache.
Simplifica aplicação.
Também esconde chamadas à origem atrás da semântica de cache.
Use quando a plataforma padroniza:
- construção de chave,
- loader,
- política de TTL,
- coalescing,
- métricas,
- comportamento stale.
Evite quando chamadores precisam de regras de consistência diferentes.
Write-Through
Write-through atualiza cache e fonte no caminho do request.
sequenceDiagram
participant Client
participant Service
participant DB
participant Cache
Client->>Service: PATCH /products/123
Service->>DB: UPDATE product
DB-->>Service: commit ok
Service->>Cache: SET product:123 new value
Cache-->>Service: ok
Service-->>Client: 200 OK
Write-through reduz leitura stale após write.
Também aumenta latência de escrita.
Ainda pode errar se ordem de commit e cache for mal definida.
O commit no banco deve continuar autoritativo.
Write-Behind
Write-behind grava cache primeiro e persiste depois.
É perigoso para dado de negócio normal.
Use só quando perda é aceitável ou existe fila durável.
Exemplos:
- contadores efêmeros,
- agregação de telemetria,
- métricas derivadas de baixo valor,
- writes bufferizados por log append-only.
Nunca use cache write-behind simples para saldo, pedido, permissão ou auditoria.
Refresh-Ahead
Refresh-ahead renova chaves quentes antes de expirar.
Reduz tail latency para objetos previsíveis.
Pode desperdiçar origem se aplicado sem critério.
Use para:
- home page,
- páginas críticas de catálogo,
- configuração de tenant muito lida,
- chaves com popularidade estável.
Sharding e Roteamento
Um nó de cache é gargalo vertical.
Cache distribuído exige roteamento.
Opções de Roteamento
| Opção | Como funciona | Benefício | Custo |
|---|---|---|---|
| consistent hashing no cliente | cliente mapeia chave para nó | baixa latência | complexidade no cliente |
| proxy | proxy mapeia chave para nó | controle central | hop extra |
| Redis Cluster | hash slots e redirects | modelo nativo | cliente precisa MOVED/ASK |
| roteamento por serviço | app escolhe pool por classe | isolamento | política operacional maior |
Consistent Hashing
flowchart TB
KEY["chave de cache"] --> HASH["hash(key)"]
HASH --> RING["anel de consistent hashing"]
RING --> N1["nó A"]
RING --> N2["nó B"]
RING --> N3["nó C"]
RING --> N4["nó D"]
N2 -. no_removido .-> MOVE["só ranges vizinhos se movem"]
Consistent hashing reduz movimento de chaves quando nós mudam.
Não elimina movimento.
Sistemas grandes usam virtual nodes ou pesos para suavizar distribuição.
Rendezvous Hashing
Rendezvous hashing calcula score por nó e escolhe o maior.
É simples e funciona bem em bibliotecas de cliente.