Em escala pequena, cache parece atalho. Em produção, cache vira sistema distribuído
Recursos selecionados para complementar sua leitura

Software Architect
Pós-graduado em arquitetura de software e soluções. Conecto profundidade técnica com resultados de negócio para entregar produtos que as pessoas realmente usam. Também mentoro desenvolvedores e criadores em programas ao vivo, podcasts e iniciativas de comunidade focadas em tecnologia inclusiva.
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Continue explorando tópicos similares

Um guia completo de system design para construir uma plataforma de compartilhamento de corridas lidando com milhões de viagens simultâneas, matching geoespacial em tempo real, precificação dinâmica e predição de ETA em escala global.

Um guia de arquitetura em nível de produção para construir um sistema estilo Instagram com feed personalizado, stories, reels, mensagens, notificações, ranking em tempo real e operação global.

Guia de system design para construir uma plataforma Open Finance no Brasil, cobrindo BACEN, consentimento granular, FAPI 2.0, Pix, arquitetura event-driven e compliance.
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.
Consultorias modulares para founders e CTOs fracionados. Você recebe diagnóstico acionável e acompanhamento direto comigo.
2 vagas para consultorias no Q2

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:
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.
A primeira decisão em entrevista é escopo. Não comece por Redis. Comece pelo workload.
| 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 |
Este desenho assume:
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.
Declare premissas antes de números. O número exato importa menos que o formato da pressão.
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
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.
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.
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.
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.
Design de cache distribuído é design para limitar dano em miss. O caminho feliz é simples. O caminho de miss decide se o sistema sobrevive.
| 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 |
API de cache distribuído tem duas camadas:
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.
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>;
}
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;
}
);
}
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
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
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 cache começa por chave. Chave ruim cria stale data, vazamento cross-tenant, partição quente e migração dolorosa.
<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
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.
| 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 |
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:
Cache-aside é o padrão mais comum. A aplicação controla miss.
Benefícios:
Custos:
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:
Evite quando chamadores precisam de regras de consistência diferentes.
Write-through atualiza cache e fonte no caminho do request.
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 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:
Nunca use cache write-behind simples para saldo, pedido, permissão ou auditoria.
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:
Um nó de cache é gargalo vertical. Cache distribuído exige 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 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 calcula score por nó e escolhe o maior. É simples e funciona bem em bibliotecas de cliente.
function pickNode(key: string, nodes: string[]): string {
let bestNode = nodes[0];
let bestScore = Number.NEGATIVE_INFINITY;
for (const node of nodes) {
const score = hash64(`${key}:${node}`);
if (score > bestScore) {
bestScore = score;
bestNode = node;
}
}
return bestNode;
}
Twemproxy, ou nutcracker, é um exemplo histórico de proxy para Redis e Memcached. Ele reduzia conexões no backend, suportava sharding e expunha stats por porta de monitoramento. O trade-off é que o proxy vira parte crítica do data plane.
Use proxy quando:
Prefira roteamento no cliente quando:
| Falha | Sintoma | Mitigação |
|---|---|---|
| ring stale no cliente | misses sobem após mudança | versão de ring e push rápido |
| proxy sobrecarregado | latência global sobe | autoscale e shed de baixa prioridade |
| hash ruim | shard quente | revisão de chaves e split |
| reshard storm | misses e redirects | migração gradual e warmup |
| roteamento cross-AZ | custo e latência sobem | cliente topology-aware |
Redis Cluster divide keyspace em 16.384 hash slots.
Cada master possui um subconjunto de slots.
Clientes normalmente roteiam direto para o nó dono do slot.
Redis Cluster pode redirecionar clientes com MOVED e ASK.
HASH_SLOT = CRC16(key) mod 16384
Hash tags permitem colocar chaves relacionadas no mesmo slot.
user:{123}:profile
user:{123}:settings
user:{123}:permissions
Só o trecho dentro de {...} entra no hash de slot.
MOVED e ASK.Hash tag resolve localidade. Também pode concentrar tráfego. Ruim:
tenant:{tenant_42}:all_products
tenant:{tenant_42}:all_orders
tenant:{tenant_42}:all_users
Melhor:
tenant:tenant_42:product:{product_123}
tenant:tenant_42:order:{order_900}
tenant:tenant_42:user:{user_77}
Use hash tag para operação multi-key atômica. Não use como hábito de namespace.
Memcached é propositalmente simples. Clientes conhecem lista de servidores. Clientes fazem hash da chave para escolher servidor. Servidores não coordenam entre si. Essa simplicidade é o produto.
Objetos Memcached incluem:
O servidor não entende JSON, Protobuf, schema de domínio ou relação entre objetos.
O paper da Facebook na NSDI continua útil porque trata memcache como sistema distribuído construído de peças simples. Lições importantes:
Memcached usa slab classes para alocação de memória. Itens de tamanho parecido compartilham classes. Isso melhora velocidade de alocação, mas desperdiça memória quando tamanhos não encaixam bem.
Memcached é LRU-style por padrão. Um item pode ser evictado antes do TTL se a slab class dele ficar sem chunks livres. TTL não garante sobrevivência. TTL é tempo máximo de vida, não mínimo.
Use Memcached quando:
Use Redis quando:
Replicação melhora disponibilidade. Não transforma cache em banco.
Redis normalmente usa primary-replica. Redis Cluster usa replicação assíncrona entre masters e replicas. Replicação assíncrona significa que primary pode confirmar write antes da réplica receber. Se primary falhar nessa janela, write pode ser perdido. Para cache, isso costuma ser aceitável. Para fonte de verdade, não.
Ler de réplica aumenta throughput. Também pode retornar valor antigo. Use para:
Evite para:
| Evento | Comportamento esperado | Comportamento da aplicação |
|---|---|---|
| nó de cache fora | misses parciais ou promoção | fallback e coalescing |
| failover Redis primary | erros breves e redirects | retry com jitter e timeout |
| nó Memcached perdido | chaves daquele nó viram miss | fallback e warmup gradual |
| proxy indisponível | erros amplos de cache | bypass ou fail soft |
| lag no event bus | stale dura mais | medir frescor e lag |
| banco degradado | miss path perigoso | servir stale se política permitir |
Caches multi-região normalmente são regionais, não globalmente síncronos. Cache deve ficar perto da aplicação. Ler cache em outra região geralmente destrói o ganho de latência.
| Estratégia | Latência | Freshness | Complexidade | Uso |
|---|---|---|---|---|
| cache regional independente | baixa | eventual | média | maioria dos sistemas read-heavy |
| cache global central | alta fora da região | pode ser mais forte | alta | dado central raro |
| eventos de invalidação | baixa | limitada por lag | média | default comum |
| só TTL local | baixa | mais fraca | baixa | dado não crítico |
| write-through em todas regiões | variável | melhor | alta | pequeno conjunto crítico |
Defina frescor em segundos. Não diga apenas "eventual". Exemplo:
Página de catálogo:
- TTL regional: 900 segundos
- lag p99 de invalidação: 10 segundos
- stale aceitável: 30 segundos
- alerta se lag > 60 segundos
O maior risco é staleness invisível. Cada região pode ter hit rate alto servindo valor antigo. Monitore versão de fonte, lag de evento e idade do objeto.
Só existem três ferramentas amplas de invalidação:
Sistemas de produção combinam as três.
function jitterTtl(baseSeconds: number, jitterRatio: number): number {
const spread = baseSeconds * jitterRatio;
const min = baseSeconds - spread;
const max = baseSeconds + spread;
return Math.floor(min + Math.random() * (max - min));
}
Jitter evita expiração simultânea de muitas chaves.
Chaves versionadas evitam delete massivo.
catalog:v4:product:123
catalog:v5:product:123
Deploy de v5 abandona v4 naturalmente.
Chaves antigas expiram depois.
Esse padrão é ótimo para mudança de schema.
Não resolve mudança de dado sozinho, a menos que versão de dado entre na chave.
| Granularidade | Exemplo | Benefício | Risco |
|---|---|---|---|
| chave única | product:123 | preciso | exige mapear chave |
| família | product:123:* | pega variantes | scan/delete pode ser caro |
| versão de namespace | catalog:v5 | migração fácil | cold start |
| versão por tenant | tenant:42:v12 | invalidação ampla | chave de versão pode ficar quente |
| evento | ProductUpdated | auditável | lag de evento |
Não dependa de KEYS product:* em produção.
No Redis, SCAN serve para manutenção cuidadosa, não para invalidação no request path.
Opções melhores:
Eviction não é detalhe de background. É o cache escolhendo quais objetos perdem.
Redis usa maxmemory-policy para decidir eviction.
Políticas comuns:
noeviction,allkeys-lru,allkeys-lfu,allkeys-random,volatile-lru,volatile-lfu,volatile-random,volatile-ttl.Políticas volatile-* só removem chaves com TTL.
Se não houver chave elegível, o comportamento pode ficar parecido com noeviction.
LRU do Redis é aproximado. Ele amostra chaves em vez de manter lista global perfeita. Isso economiza memória e CPU. Também torna eviction probabilística. Só ajuste depois de medir.
Buffers de replicação e persistência no Redis podem consumir memória fora do dataset comparado com maxmemory.
Deixe headroom.
Não configure maxmemory igual à RAM total da máquina.
Memcached aloca memória em slab classes. Uma classe pode evictar enquanto outra tem memória livre. Isso surpreende quem observa só memória global. Monitore evictions por slab.
Fragmentação faz used_memory e RSS divergirem.
Causas comuns:
Resposta operacional:
Política recomendada:
- rejeitar valores acima de 1 MB por padrão
- avisar acima de 128 KB
- comprimir acima de 8 KB só depois de medir
- isolar cache de objetos grandes do cache de objetos pequenos e quentes
Valores grandes reduzem densidade efetiva do cache. Também aumentam latência de rede e risco de cauda.
Consistência de cache é decisão de produto expressa em infraestrutura.
| Semântica | Significado | Implementação típica |
|---|---|---|
| cache pode ficar stale | leitura pode atrasar fonte | TTL e invalidação |
| read-your-writes | autor vê própria atualização | bypass local ou update pós-commit |
| leituras monotônicas | usuário não volta no tempo | checks de versão |
| staleness limitado | janela máxima definida | TTL + SLO de lag |
| consistência forte | cache sempre igual à fonte | geralmente evite |
Alguns sistemas deletam antes e depois do write no banco. Isso pode reduzir race de repopulação stale. Não prova correção. Também adiciona pressupostos temporais. Prefira outbox durável e checks de versão para caminhos críticos.
Usar cache como banco começa silenciosamente:
Redis pode ter persistência. Isso não transforma automaticamente um deploy de cache em banco durável. Durabilidade exige garantias de write, RPO/RTO, backup validado, ownership operacional e modelagem adequada.
Hot key é uma chave cujo tráfego passa da capacidade confortável de um shard. É comum. Exemplos:
| Técnica | Como funciona | Trade-off |
|---|---|---|
| cache L1 local | mantém valores quentes no processo | invalidação mais difícil |
| replicação de chave | mesmo valor em N chaves | mais memória e fanout |
| request coalescing | um load por chave | não reduz tráfego de hit |
| counters shardados | divide writes em chaves | leitura precisa agregação |
| CDN/edge cache | move leitura para borda | só para dados HTTP-safe |
| isolamento de tenant | tenant grande em pool próprio | mais operação |
| rate limiting | corta abuso | pode afetar cliente |
function hotReplicaKey(baseKey: string, replicas: number): string {
const replica = Math.floor(Math.random() * replicas);
return `${baseKey}:replica:${replica}`;
}
Em leitura, escolha réplica aleatória. Em write ou invalidação, atualize ou delete todas. Funciona para valores read-heavy quase imutáveis. É ruim para valor que muda muito.
Use TTLs minúsculos no L1. Exemplos:
L1 pode multiplicar stale por milhares de processos. Use com escopo estreito.
Thundering herd acontece quando muitos requests dão miss juntos e batem na origem. Dogpile acontece quando uma chave quente expira e muitos requests recarregam ao mesmo tempo.
const inFlight = new Map<string, Promise<unknown>>();
async function coalesced<T>(key: string, loader: () => Promise<T>): Promise<T> {
const existing = inFlight.get(key) as Promise<T> | undefined;
if (existing) return existing;
const promise = loader().finally(() => {
inFlight.delete(key);
});
inFlight.set(key, promise);
return promise;
}
Coalescing local ajuda dentro de uma instância. Locks distribuídos ou leases ajudam entre instâncias.
O paper de memcache do Facebook descreve leases para controlar stale sets e reduzir stampedes. Ideia geral:
soft TTL: momento em que refresh deve começar
hard TTL: momento em que valor não pode mais ser servido
Negative caching armazena misses. Exemplo:
negative:v1:product:missing_123 => "not_found" EX 30
Isso protege o banco contra consultas repetidas por objetos inexistentes.
Se TTL for longo, objetos recém-criados podem parecer ausentes. Se invalidação faltar, creates podem não aparecer. Se keyspace for atacável, atacante pode preencher memória com chaves negativas.
Não cacheie tudo. Admission control decide se valor merece memória.
Falha de cache deveria degradar performance, não correção. Essa frase é fácil. Ela exige engenharia explícita.
Meta p99 da API: 150ms
Budget de lógica do serviço: 40ms
Budget de fallback no banco: 80ms
Budget de cache get: 3ms
Budget de cache set: fire-and-forget ou 5ms no máximo
Nunca deixe chamada de cache consumir o budget inteiro do fallback.
Use circuit breakers ao redor de:
Estados:
| Uso do cache | Comportamento com cache fora |
|---|---|
| product page model | bypass, DB, talvez stale |
| feature flags | último snapshot local por pouco tempo |
| permissões | bypass e fonte de verdade |
| contador de rate | fallback conservador depende do risco |
| idempotency key | não use só cache volátil |
| token de sessão | depende do auth; prefira store adequado |
Caches frequentemente guardam dados derivados sensíveis. Trate-os como stores de produção em segurança, mesmo sem serem fonte de verdade.
Hit rate é útil. Hit rate não basta. Cache com 99% de hit rate pode estar quebrado se 1% de misses derruba o banco.
Monitore por namespace, classe de objeto, região e tier de tenant:
| SLO | Meta |
|---|---|
| disponibilidade de cache get | 99,99% |
| p99 de cache get dentro da região | < 2ms |
| timeout rate de cache | < 0,1% |
| lag p99 de invalidação | < 10s |
| stale além do budget | < 0,01% das leituras |
| miss QPS na origem | abaixo do limite seguro |
| detecção de hot key | < 60s |
Persistência do Redis não é plataforma de dados completa por si só. Se dado importa, desenhe durabilidade explicitamente.
Objetos diferentes têm freshness diferente. TTL único fica stale demais para dado sensível ou curto demais para dado estável.
Expirações alinhadas criam stampede previsível. Use jitter.
Chaves de baixo reuso desperdiçam memória. Respostas grandes one-off expulsam objetos úteis.
Hit rate pode parecer bom enquanto byte hit rate é ruim. Meça ambos.
KEYS em ProduçãoScan global pode bloquear ou sobrecarregar Redis. Desenhe invalidação sem scan no request path.
Cache não pode consumir todo budget de latência da API. Use timeout curto e fallback.
Miss path precisa de coalescing, rate limit ou backpressure em escala.
Hash tags no Redis forçam localidade. Também podem criar hot slots.
Valores grandes e frios podem expulsar valores pequenos e quentes. Use isolamento de pool.
Hit rate sem carga na origem, frescor e latência é incompleto.
Permissão stale é bug de segurança. Use TTL curto, versão ou leitura direta da fonte.
Cache distribuído é sistema de controle de latência e carga. Redis e Memcached são escolhas de implementação dentro desse sistema. A verdade durável continua em outro lugar. O design de produção mora nas bordas:
Se você explica esses trade-offs, consegue desenhar um cache que aguenta tráfego real. Não apenas um cache que melhora benchmark.
| Necessidade | Padrão |
|---|---|
| aceleração simples de leitura | cache-aside |
| loading gerenciado pela plataforma | read-through |
| cache fresco depois de writes | write-through após commit |
| persistência assíncrona de baixo valor | write-behind com fila durável |
| dado quente previsível | refresh-ahead |
| IDs inexistentes repetidos | negative caching |
| hot keys expirando | stale-while-revalidate |
| Dimensão | Redis | Memcached |
|---|---|---|
| estruturas | ricas | bytes simples |
| sharding | Redis Cluster hash slots | hashing no cliente/proxy |
| replicação | nativa | não nativa |
| persistência | opcional | não típica |
| scripts atômicos | sim | não |
| operação | mais recursos e knobs | mais simples |
| melhor uso | cache rico e estruturas efêmeras | cache simples de objetos |
Eu desenharia um cache distribuído cache-aside na frente do banco fonte.
Serviços usam biblioteca comum para chave, TTL, serialização, coalescing e métricas.
Redis Cluster ou Memcached shardado no cliente entrega escala horizontal.
Chaves incluem namespace, versão de schema, tenant, entidade, id e variante.
Writes fazem commit no banco primeiro e publicam evento de invalidação via outbox.
TTL com jitter limita stale e evita expiração sincronizada.
Miss path usa request coalescing, stale-while-revalidate, negative caching e admission control.
Hot keys são detectadas por amostragem e tratadas com L1, replicação, split ou isolamento.
Confiabilidade usa timeouts curtos, circuit breakers, fallback na origem e stale controlado.
Observabilidade mede latência, hit rate, miss QPS, evictions, memória, fragmentação, hot keys e lag.
Cache nunca é fonte de verdade sem redesenhar durabilidade, backup e consistência.