1

Projetando um Cache Distribuído: Guia Completo de System Design

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

A primeira decisão em entrevista é escopo.
Não comece por Redis.
Comece pelo workload.

Requisitos Funcionais

  1. Servir objetos lidos com frequência com latência menor que a fonte de verdade.
  2. Reduzir pressão de leitura em bancos primários e APIs downstream.
  3. Suportar leituras e escritas key-value para objetos serializados.
  4. Suportar expiração por TTL.
  5. Suportar invalidação explícita depois de writes.
  6. Suportar fallback seguro em cache miss ou falha do cache.
  7. Suportar escala horizontal entre nós de cache.
  8. Suportar chaves de alta cardinalidade sem posicionamento manual.
  9. Suportar mitigação de hot keys.
  10. Expor métricas de hit rate, latência, memória, evictions e erros.
  11. Suportar controle de acesso e transporte criptografado quando necessário.
  12. Manter comportamento previsível durante deploys, failovers e resharding.

Requisitos Não Funcionais

RequisitoMetaPor que importa
Latência de get< 2ms p99 dentro da regiãocache precisa ganhar do banco
Disponibilidade do cache99,99% no caminho de leiturafalha não pode derrubar app
Hit rate80-95% para objetos cacheáveisabaixo disso custo pode não fechar
Tolerância a staleexplícita por objetocada dado envelhece de forma diferente
Eficiência de memóriamedida por classe de objetomemória é custo principal
Tempo de failoversegundos a poucos minutosincidente longo sobrecarrega banco
Estabilidade de roteamentopouco movimento em mudança de nóresharding não deve esfriar tudo
Clareza operacionalalertas ligados a impactohit rate sozinho engana

Perguntas de Clarificação

  1. Quais dados podem ser cacheados?
  2. Quanto stale cada objeto pode ficar?
  3. Cache é local a um serviço ou compartilhado por vários serviços?
  4. Read-through mora numa biblioteca, proxy ou aplicação?
  5. Qual distribuição de tamanho dos objetos?
  6. Qual QPS de pico e qual QPS de miss?
  7. O que acontece se o cache cair?
  8. Consistência cross-region é obrigatória?
  9. Dados exigem criptografia ou isolamento forte?
  10. 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

  1. Mantenha leitura correta sem cache.
  2. Deixe uso de cache explícito no limite do serviço.
  3. Centralize construção de chaves em uma biblioteca.
  4. Use cache local só para objetos pequenos, seguros e curtos.
  5. Use cache distribuído para objetos quentes compartilhados.
  6. Use invalidação por evento para famílias com muita escrita.
  7. Use TTL como rede de segurança, não como única correção.
  8. Proteja misses com coalescing e backpressure.
  9. Trate hot key como problema central de capacidade.
  10. Meça frescor, não só hit rate.

Escolha de Cache por Workload

WorkloadMelhor encaixeObservação
objetos serializados simplesMemcachedrápido, simples, shardado no cliente
contadores e estado atômicoRedisoperações atômicas e scripts
feeds ordenados ou rankingsRedissorted sets ajudam, custo de memória alto
sessões ou estado efêmeroRedis ou Memcacheddepende de durabilidade esperada
decisões de autorizaçãogeralmente evitaruse versões e TTL curto quando necessário
read models carosRedis ou Memcachedcombine com eventos de invalidação
blobs grandesgeralmente evitarguarde 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

  1. Inclua tenant ou fronteira de isolamento.
  2. Inclua versão de schema.
  3. Inclua região quando a região muda resposta.
  4. Inclua segmento de viewer quando personalização muda resposta.
  5. Mantenha chaves curtas para reduzir gasto de memória.
  6. Evite strings livres vindas do usuário.
  7. Evite chaves que exigem scan global.
  8. Evite dados sensíveis em texto claro na chave.
  9. Prefira builders determinísticos a concatenação manual.
  10. 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

ClasseTTLInvalidaçãoPadrãoObservação
product page model15 minevento após updatecache-asidestale curto aceitável
perfil público10 minevento após updatecache-asideincluir versão de privacidade
feature flags30 spush por streamread-through/localcorreção importa
permissões5-30 schave versionadacache-asidestale grant é risco
produto inexistente30 sevento de create opcionalnegative cacheevita misses repetidos
relatório caro1 hdelete explícitowrite-throughobservar 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çãoComo funcionaBenefícioCusto
consistent hashing no clientecliente mapeia chave para nóbaixa latênciacomplexidade no cliente
proxyproxy mapeia chave para nócontrole centralhop extra
Redis Clusterhash slots e redirectsmodelo nativocliente precisa MOVED/ASK
roteamento por serviçoapp escolhe pool por classeisolamentopolí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.

Carregando publicação patrocinada...