Cache Distribuído
"Projete um cache distribuído em memória como o Memcached ou o Redis Cluster: um armazenamento chave-valor que muitos servidores de aplicação usam para não bater no banco, guardando mais dados do que cabem numa máquina, com latência abaixo de um milissegundo."
Quase todo caso desta disciplina termina com "e coloca um cache na frente". Este caso abre essa caixa. Um cache distribuído é um pequeno banco de dados distribuído com regras relaxadas: ele pode perder dados (a fonte da verdade está em outro lugar), mas precisa ser rápido, espalhar as chaves de forma equilibrada por muitos nós, sobreviver a nós entrando e saindo sem um estouro de manada no banco e lidar com chaves muito mais quentes que as outras. É a entrevista em que hash consistente, políticas de despejo, atraso de replicação e chaves quentes aparecem num só design — as mesmas ideias por trás do Dynamo, do Cassandra e do Redis Cluster.
No BoxOffice, o cache guarda páginas de eventos, retratos da disponibilidade de lugares e sessões. Às 10:00 de um dia de abertura de vendas ele absorve milhões de leituras por minuto; se um nó de cache morrer às 10:01, a questão é se sua fatia do tráfego passa suavemente para os outros ou cai inteira sobre o PostgreSQL de uma vez.
| Dificuldade | médio |
| Conceitos-chave | particionamento com hash consistente e nós virtuais, hash slots, despejo (LRU, LFU, W-TinyLFU) e TTL, dimensionamento de memória, replicação primário/réplica e failover, split brain, chaves quentes e chaves grandes, roteamento no cliente vs. proxy, cache-aside, proteção contra estouro de manada (leases, coalescência de requisições, expiração antecipada) |
| Componentes da paleta | cache, app, nosql, metrics |
| Cenários do playground | read-heavy-cache, sharding |
O que este caso treina
- Dimensionar um cluster de cache pela memória e pelas operações por segundo, e relacionar a taxa de acerto com a carga no banco.
- Explicar por que
hash(chave) % Nfalha quando os nós mudam e como o hash consistente com nós virtuais resolve. - Escolher uma política de despejo e TTLs para uma carga, e explicar o que o W-TinyLFU acrescenta ao LRU.
- Projetar replicação e failover, e declarar o que se perde com replicação assíncrona.
- Mitigar chaves quentes, chaves grandes e estouros de manada no cache.
1. Esclarecer os requisitos
Requisitos funcionais
get(key),set(key, value, ttl),delete(key); operações atômicas opcionais (incr, compare-and-set).- As chaves expiram depois do TTL; quando a memória enche, o cache despeja algumas chaves.
- O conjunto de dados é maior que uma máquina: as chaves são particionadas entre muitos nós.
- Nós podem ser acrescentados ou removidos (escalonamento, falhas) sem reiniciar os clientes.
Requisitos não funcionais
| Qualidade | Meta |
|---|---|
| Latência | p99 < 1 ms dentro do data center |
| Vazão | ~2 M de operações/s no pico, crescendo |
| Capacidade | ~1 TB de dados quentes |
| Disponibilidade | A falha de um nó não pode causar indisponibilidade nem estouro de manada no banco |
| Consistência | Eventual; algumas leituras desatualizadas após uma escrita são aceitáveis (a fonte da verdade é o banco) |
| Durabilidade | Não exigida — perder os dados de um nó custa faltas no cache, não corretude |
Fora do escopo (diga explicitamente)
Persistência em disco (snapshots/AOF), replicação entre regiões, estruturas de dados complexas além de strings e contadores, autenticação — cite que o Redis oferece várias delas.
Perguntas para fazer ao entrevistador
| Pergunta | Por que importa |
|---|---|
| É um cache (dados podem se perder) ou um armazenamento primário? | Um cache pode usar replicação assíncrona e despejar à vontade; um armazenamento não. |
| Tamanho total dos dados e tamanho dos valores? | Dimensionamento de memória; valores grandes (MBs) exigem cuidado especial. |
| Razão leitura/escrita e pico de ops/s? | Muitas vezes são as ops/s, e não a memória, que decidem o número de nós. |
| Dados desatualizados são aceitáveis? Por quanto tempo? | TTLs, estratégia de invalidação, leitura em réplicas. |
| Algumas chaves são muito mais quentes que outras? | Chaves quentes quebram o particionamento equilibrado. |
| Quem roteia: a biblioteca cliente, um proxy ou os servidores? | Afeta latência, complexidade do cliente e operação. |
2. Estimativas de guardanapo (back-of-the-envelope)
| quantity | value | unit |
|---|---|---|
data_tb | 1.10 | TB |
nodes_memory | 17.19 | nodes |
nodes_ops | 25.00 | nodes |
primaries | 25.00 | nodes |
total_nodes | 50.00 | nodes |
net_gb_s | 2.00 | GB/s |
db_rps | 100,000 | req/s |
db_rps_90 | 200,000 | req/s |
# Distributed cache: assumptions (illustrative)
items = 1_000_000_000 # cached objects (working set)
value_b = 1_000 # average value size
key_b = 50
overhead_b = 50 # per-item metadata, pointers, allocator slack
node_mem_gb = 64 # RAM per node usable for data (leave headroom)
ops_s = 2_000_000 # total cache operations per second at peak
ops_per_node = 80_000 # comfortable ops/s per node (single-threaded engine)
replicas = 1 # replicas per primary
hit_ratio = 0.95
data_tb = items * (value_b + key_b + overhead_b) / 1e12
nodes_memory = data_tb * 1e3 / node_mem_gb
nodes_ops = ops_s / ops_per_node
primaries = max(nodes_memory, nodes_ops)
total_nodes = primaries * (1 + replicas)
net_gb_s = ops_s * value_b / 1e9
db_rps = ops_s * (1 - hit_ratio)
db_rps_90 = ops_s * (1 - 0.90)
rows = [
("data_tb", data_tb, "TB"), ("nodes_memory", nodes_memory, "nodes"),
("nodes_ops", nodes_ops, "nodes"), ("primaries", primaries, "nodes"),
("total_nodes", total_nodes, "nodes"), ("net_gb_s", net_gb_s, "GB/s"),
("db_rps", db_rps, "req/s"), ("db_rps_90", db_rps_90, "req/s"),
]
print("| quantity | value | unit |\n|---|---:|---|")
for name, value, unit in rows:
v = f"{value:,.0f}" if abs(value) >= 100 else f"{value:,.2f}"
print(f"| `{name}` | {v} | {unit} |")
O que os números dizem:
- ~1,1 TB de dados precisam de ~17 nós pela memória, mas 2 M de ops/s precisam de 25 nós pela vazão. ⇒ dimensione pelos dois e fique com o maior: aqui a vazão decide (25 primários, 50 nós com uma réplica cada).
- 2 GB/s de valores atravessam a rede no pico — divididos por 25 nós são ~80 MB/s cada, tranquilo para placas de rede modernas, mas uma única chave grande lida milhares de vezes por segundo pode saturar o link de um nó.
- Com 95% de acerto o banco recebe 100 mil req/s; com 90% recebe 200 mil req/s. ⇒ cada ponto de taxa de acerto vale 20 mil requisições por segundo no banco. Perder um nó de 25 (4% das chaves) frio é um súbito +80 mil faltas/s para o banco — o motivo de failover e réplicas aquecidas importarem (cache).
3. API
GET key → value | nil
SET key value EX 300 → OK (TTL in seconds)
SET key value NX EX 30 → OK | nil (only if absent: locks, leases)
DEL key → 1 | 0
INCRBY key 5 → 42 (atomic counter)
MGET k1 k2 k3 → [v1, v2, v3] (batched: one round trip)
# Cluster-aware client: the key decides the node
slot = CRC16(key) mod 16384 → node owning that slot (Redis Cluster)
-MOVED 3999 10.0.1.17:6379 → slot moved: update the client's map and retry there
- A biblioteca cliente faz parte da API: ela calcula o hash da chave, mantém o mapa de partições → nós, faz pool de conexões e pipelining das requisições.
- Um
MGETcom chaves em nós diferentes vira um scatter-gather; use hash tags ({event:42}:seats,{event:42}:page) para manter no mesmo nó as chaves relacionadas que são lidas juntas.
4. Modelo de dados
# On each node: a hash table of entries + an eviction structure
entry = { key, value, expires_at, flags, lru_clock | lfu_counter }
index = hash table key → entry (O(1) get/set)
policy = LRU list / sampled LRU / LFU counters (who to evict when memory is full)
# Cluster metadata (gossiped between nodes or stored in a coordination service)
slots 0–5460 → node-a (primary), node-d (replica)
slots 5461–10922 → node-b (primary), node-e (replica)
slots 10923–16383 → node-c (primary), node-f (replica)
| Dado | Armazenamento | Por quê |
|---|---|---|
| Valores em cache | Tabela hash em memória por nó (cache) | Acesso O(1), sem disco no caminho |
| Mapa de partições (slots/anel → nós) | Gossip entre os nós, ou um serviço de coordenação (etcd/ZooKeeper); em cache em cada cliente | Pequeno, lido em toda requisição, muda raramente |
| Fonte da verdade | Banco de dados (nosql/SQL) atrás do cache | O cache sempre pode ser reconstruído a partir dele |
| Taxa de acerto, despejos, memória, latência por nó | Métricas (metrics) | Nós quentes, tempestades de despejo e pressão de memória só aparecem aqui |
5. Design de alto nível
flowchart LR
app1["Servidor de app #1<br/>cliente que conhece o cluster"] --> a["node-a<br/>slots 0–5460"]
app1 --> b["node-b<br/>slots 5461–10922"]
app2["Servidor de app #2<br/>cliente que conhece o cluster"] --> b
app2 --> c["node-c<br/>slots 10923–16383"]
a -. replicação assíncrona .-> ra["réplica a'"]
b -. replicação assíncrona .-> rb["réplica b'"]
c -. replicação assíncrona .-> rc["réplica c'"]
app1 -- falta --> db[("Banco de dados<br/>fonte da verdade")]
app2 -- falta --> db
a -. métricas .-> m["Métricas"] Uma leitura com cache-aside, incluindo um slot que mudou de nó:
sequenceDiagram
autonumber
participant A as App (biblioteca cliente)
participant B as node-b
participant C as node-c
participant D as Banco de dados
A->>A: slot = CRC16("event:42") mod 16384 → node-b (mapa em cache)
A->>B: GET event:42
B-->>A: -MOVED 9001 node-c (o slot foi migrado)
A->>A: atualiza o mapa de slots
A->>C: GET event:42
C-->>A: nil (falta)
A->>D: SELECT evento 42
D-->>A: linha
A->>C: SET event:42 … EX 300 6. Aprofundamentos (deep dives)
6.1 Particionamento: por que hash % N quebra, e o hash consistente
Com nó = hash(chave) % N, passar de 4 para 5 nós muda o dono de quase todas as chaves — um cache com 80% das chaves de repente no lugar errado é, para o banco, o mesmo que um cache vazio. O hash consistente coloca nós e chaves no mesmo anel de valores de hash; uma chave pertence ao primeiro nó no sentido horário. Acrescentar um nó só toma chaves dos vizinhos — cerca de 1/N delas. Cada nó físico recebe muitos nós virtuais (pontos no anel) para a carga ficar equilibrada e as chaves de um nó que falhou se espalharem por todos os outros, não só por um vizinho (particionamento).
| scheme | keys_moved_pct | max_load_vs_avg |
|---|---|---|
hash % N | 80.1% | 1.01 |
ring, 1 vnodes/node | 20.9% | 1.96 |
ring, 10 vnodes/node | 17.5% | 1.53 |
ring, 100 vnodes/node | 19.9% | 1.14 |
ring, 1000 vnodes/node | 20.7% | 1.04 |
import bisect
import hashlib
def h(s: str) -> int:
return int.from_bytes(hashlib.md5(s.encode()).digest()[:8], "big")
keys = [f"event:{i}" for i in range(50_000)]
kh = {k: h(k) for k in keys}
def mod_n(n):
return {k: kh[k] % n for k in keys}
def ring(nodes, vnodes):
points = sorted((h(f"node{n}#{v}"), n) for n in range(nodes) for v in range(vnodes))
hashes = [p for p, _ in points]
owner = {}
for k in keys:
i = bisect.bisect(hashes, kh[k]) % len(points)
owner[k] = points[i][1]
return owner
def moved(a, b):
return sum(a[k] != b[k] for k in keys) / len(keys)
def max_over_avg(owner, nodes):
counts = [0] * nodes
for n in owner.values():
counts[n] += 1
return max(counts) / (len(keys) / nodes)
print("| scheme | keys_moved_pct | max_load_vs_avg |\n|---|---:|---:|")
a, b = mod_n(4), mod_n(5)
print(f"| `hash % N` | {moved(a, b):.1%} | {max_over_avg(b, 5):.2f} |")
for v in (1, 10, 100, 1000):
a, b = ring(4, v), ring(5, v)
print(f"| `ring, {v} vnodes/node` | {moved(a, b):.1%} | {max_over_avg(b, 5):.2f} |")
Leia a tabela em duas colunas. Chaves movidas ao passar de 4 para 5 nós: hash % N move ~80%; o anel move ~20% — o mínimo teórico, 1/5. Equilíbrio de carga (o nó mais ocupado em relação à média): com um ponto por nó o anel fica muito desigual (o nó mais ocupado guarda quase o dobro da sua parte); com centenas de nós virtuais ele se aproxima de 1,0.
Usado pelo Dynamo, pelo Cassandra e por muitas bibliotecas cliente (ex.: ketama para o Memcached). Cada nó é dono de muitos arcos; acrescentar/remover um nó move ~1/N das chaves, tiradas de ou entregues a todos os outros nós.
O espaço de chaves é dividido num número fixo de slots (16 384); cada nó é dono de um conjunto de slots. O rebalanceamento move slots, não faixas de hash individuais, e o cluster avisa os clientes com redirecionamentos MOVED/ASK. O mesmo benefício do anel (só os slots movidos mudam de dono), mais simples de raciocinar e de migrar aos poucos.
Para cada chave, calcule hash(chave, nó) para todos os nós e escolha o maior. Sem anel, movimento perfeito de 1/N, mas O(N) por consulta — bom para dezenas de nós.
6.2 Despejo, TTL e memória
Quando a memória enche, algo precisa sair. A política decide a taxa de acerto que você obtém com a mesma RAM (modos de falha de cache):
| Política | Mantém | Boa para | Ponto fraco |
|---|---|---|---|
| LRU (menos usada recentemente) | Chaves usadas recentemente | Cargas guiadas por recência | Uma varredura avulsa de muitas chaves expulsa o conjunto quente |
| LFU (menos usada com frequência) | Chaves usadas com frequência | Popularidade estável | Demora a esquecer os sucessos de ontem; precisa de decaimento |
| W-TinyLFU (Caffeine) | Uma pequena "janela" LRU + um filtro de admissão por frequência aproximada | Cargas mistas; resiste a varreduras | Mais complexa; principalmente em caches em processo |
| Só TTL | Tudo até expirar | Conjuntos de dados pequenos e limitados | A memória cresce com o tráfego |
| Aleatória | Chaves aleatórias | Muito barata | Taxa de acerto menor |
- Motores reais aproximam: o Redis amostra algumas chaves e despeja a melhor candidata entre elas (LRU ou LFU aproximados, com contadores que decaem) em vez de manter uma ordem global perfeita.
- TTL é ferramenta de consistência, despejo é ferramenta de memória. Sempre defina um TTL (para o dado desatualizado ter um limite) e acrescente jitter aos TTLs gravados em massa (ex.: 300 s ± 10%) para não expirarem no mesmo segundo.
- Deixe folga: planeje ~70–80% da RAM para dados. Fragmentação, buffers de replicação e o fork para snapshots precisam do resto; um nó que faz swap para o disco é mais lento do que nenhum cache.
6.3 Replicação e failover
sequenceDiagram
autonumber
participant A as App
participant P as Primário (node-b)
participant R as Réplica (node-e)
participant S as Detector de falhas
A->>P: SET seats:42 v7
P-->>A: OK
P--)R: replica v7 (assíncrono)
Note over P: o primário cai antes de v7 chegar à réplica
S->>S: node-b perdeu heartbeats → a maioria concorda
S->>R: promove node-e a primário
A->>R: GET seats:42
R-->>A: v6 (a escrita confirmada v7 se perdeu) - A replicação costuma ser assíncrona: escritas rápidas, mas um failover pode perder as últimas escritas confirmadas. Para um cache isso é aceitável — a próxima falta recarrega do banco. Se um valor desatualizado for perigoso (disponibilidade de lugares), mantenha TTLs curtos ou apague as chaves na escrita (invalidação).
- A detecção de falhas precisa de maioria (Redis Sentinel, ou os nós do cluster votando) para que um observador isolado não promova uma réplica sozinho.
- Split brain: depois de uma partição, o primário antigo ainda pode aceitar escritas de clientes do lado dele. Mitigue fazendo o primário parar de aceitar escritas quando não enxerga réplicas/pares suficientes (
min-replicas-to-write), e com épocas/termos para clientes e nós rejeitarem o primário velho. - Réplicas aquecidas também atendem leituras (se leituras desatualizadas forem aceitáveis) e evitam o estouro de manada do nó frio depois do failover.
6.4 Chaves quentes, chaves grandes e estouros de manada
- Chave quente — uma chave (a página do evento durante uma abertura de vendas, o perfil de uma celebridade) recebe tantas requisições que seu nó satura enquanto os outros ficam ociosos. O hash consistente não ajuda: uma chave mora num nó só. Soluções: um pequeno cache L1 em processo com TTL de segundos em cada servidor de aplicação; replicar a chave com vários nomes (
event:42#1 … #8) e ler uma cópia aleatória; atendê-la pelas réplicas. - Chave grande — um valor de 5 MB lido 1 000 vezes por segundo são 5 GB/s num nó. Divida-o (pagine, comprima), ou guarde em cache os fragmentos renderizados em vez do objeto inteiro.
- Estouro de manada (thundering herd) — uma chave quente expira e milhares de requisições falham ao mesmo tempo e vão todas ao banco. Soluções (modos de falha de cache):
- Coalescência de requisições / leases: a primeira falta ganha uma lease (
SET lock:key NX EX 5) e recarrega; as outras esperam um pouco ou recebem o valor velho. O Memcache do Facebook usou leases exatamente para isso. - Expiração antecipada probabilística: cada leitor pode renovar antes do TTL com uma probabilidade que cresce conforme a expiração se aproxima, então uma requisição renova antes em vez de todas ao mesmo tempo.
- Servir o velho enquanto revalida para dados em que alguns segundos de desatualização tudo bem.
- Coalescência de requisições / leases: a primeira falta ganha uma lease (
import math, random, time
def should_refresh_early(expires_at: float, recompute_s: float, beta: float = 1.0) -> bool:
"""XFetch: refresh before expiry with a probability that grows near the deadline."""
return time.time() - recompute_s * beta * math.log(random.random()) >= expires_at
No código do BoxOffice: um lease contra o stampede (lab 5)
O BoxOffice roda um Redis só, não um cluster, então os labs não constroem o particionamento deste caso — mas o Hands-on 5 — Stampede constrói o seu lease: a primeira falta pega o lock com SET NX e recarrega, relê depois de vencer, e as outras consultam a chave por no máximo 20 × 25 ms. No teste do lab (200 usuários virtuais por 60 s, TTL de 3 s), as faltas caíram de 2.238 para 19 — uma consulta ao banco por expiração. O passo a passo completo está em Cache — modos de falha.
if (stampedeProtection) {
var lockKey = key + ":lock";
var token = UUID.randomUUID().toString();
if (tryLock(lockKey, token)) {
try {
cached = read(key); // someone may have filled it meanwhile
if (cached != null) {
return count(new Result(cached, "HIT"));
}
return count(load(key, ttl, loader));
} finally {
unlock(lockKey, token);
}
}
// another request is already loading this key: wait for it instead of
// sending one more identical query to the database
for (int i = 0; i < 20; i++) {
sleep(Duration.ofMillis(25));
cached = read(key);
if (cached != null) {
return count(new Result(cached, "HIT"));
}
}
}
6.5 Roteamento: quem sabe onde mora uma chave?
| Roteamento no cliente (cliente esperto) | Proxy (twemproxy, mcrouter, Envoy) | Redirecionamentos do servidor (Redis Cluster) | |
|---|---|---|---|
| Saltos | 1 (cliente → nó) | 2 (cliente → proxy → nó) | 1, mais um redirecionamento após rebalancear |
| Complexidade do cliente | Alta (mapa, retentativas, pool em cada linguagem) | Baixa (parece um servidor só) | Média (cliente que conhece o cluster) |
| Mudanças de configuração | Enviadas a cada cliente | Só aos proxies | Aprendidas pelo MOVED |
| Recursos extras | — | Pool, replicação, tráfego espelhado | Migração de slots embutida |
A implantação de memcache do Facebook, por exemplo, colocou o roteamento num proxy (mcrouter) para que milhares de servidores web não precisassem cada um gerenciar conexões com todos os nós de cache.
7. Gargalos, modos de falha e trade-offs
| Onde | O que dá errado | Mitigação | Trade-off |
|---|---|---|---|
| Acrescentar/remover nós | hash % N remapeia quase todas as chaves → faltas em massa | Hash consistente ou hash slots; migrar aos poucos | Tráfego de rebalanceamento por um tempo |
| Falha de um nó | Suas chaves esfriam; pico no banco | Réplicas com failover automático; espalhar chaves com nós virtuais; o cache L1 absorve parte das leituras | Réplicas dobram o custo de memória |
| Replicação assíncrona | Escritas confirmadas perdidas no failover | Aceitar num cache; TTLs curtos ou apagar na escrita para chaves sensíveis | Valores velhos ou ausentes por pouco tempo |
| Partição de rede | Dois primários (split brain) | Failover por maioria; primário para de escrever sem quórum; épocas | Escritas rejeitadas no lado minoritário |
| Chave quente | Um nó saturado | Cache L1, replicação da chave com sufixos, leitura nas réplicas | Desatualização, mais memória |
| Chave grande | Link de rede saturado; comandos lentos bloqueiam o nó | Dividir/comprimir; evitar comandos O(N) em estruturas grandes | Mais chaves para gerenciar |
| Estouro de manada na expiração | Milhares de faltas para uma chave | Leases/coalescência, renovação antecipada, jitter no TTL, servir o velho | Leve desatualização |
| Pressão de memória | Tempestades de despejo, fragmentação, swap | Folga (~70–80%), política certa, alertas de despejos e memória | Menos bytes usados por nó |
Os trade-offs para dizer em voz alta: disponibilidade e velocidade acima de consistência (um cache pode estar desatualizado ou perder escritas; o banco é a verdade), memória vs. vazão (dimensione pelos dois), e distribuição equilibrada vs. chaves quentes (o hash espalha chaves, não popularidade).
8. Experimente no playground
Dois cenários cobrem as duas metades deste caso: o cache na frente de um banco, e o particionamento dos dados entre nós.
Experimente — cache na frente do banco
- Rode o
read-heavy-cachecom o perfil load e anote a taxa de acerto, a utilização do banco e o p99. - Baixe a taxa de acerto (ou diminua o cache) em 5 pontos e rode de novo: compare a mudança na carga do banco com a estimativa da seção 2.
- Ligue o caos para derrubar o cache (ou esvaziá-lo): veja o estouro de manada do cache frio no banco e quanto tempo o p99 leva para se recuperar.
Experimente — partições e chaves quentes
- Rode o
shardingcom o perfil load e compare a utilização entre as partições. - Concentre o tráfego numa chave/partição (uma chave quente): um nó satura enquanto os outros ficam ociosos — mais partições não ajudam.
- Acrescente partições/nós com uma carga equilibrada e observe como a vazão escala; depois derrube um nó e veja para onde vai o seu tráfego.
9. Perguntas de follow-up que o entrevistador faz
Por que não usar simplesmente hash(chave) % N? É mais simples.
Ele distribui bem as chaves enquanto N é fixo. No momento em que N muda — um nó falha, você escala para uma abertura de vendas — cerca de 1 - 1/N das chaves mudam de dono (80% ao passar de 4 para 5 nós), o que equivale a esvaziar o cache no meio do pico. Hash consistente ou slots fixos movem só as chaves que precisam mudar (~1/N).
Um nó falha durante a abertura de vendas do BoxOffice. Descreva o que acontece.
Os clientes recebem timeouts/erros de conexão para os slots daquele nó. O detector de falhas (a maioria dos nós ou dos Sentinels) o marca como falho depois de alguns segundos e promove sua réplica; os clientes descobrem o novo dono (redirecionamento ou configuração enviada) e seguem. Escritas confirmadas pelo primário antigo mas ainda não replicadas se perdem — inofensivo para um cache. Sem réplica, as chaves daquele nó seriam faltas até serem recarregadas; caches L1, coalescência de requisições e um banco com folga tornam esse pico suportável.
Como você mantém o cache consistente com o banco?
Você não o torna fortemente consistente — você limita a inconsistência. Cache-aside com: escreva no banco e depois apague a chave (não a atualize, para evitar que escritores concorrentes recoloquem um valor velho); um TTL como rede de segurança; e, para dados críticos, invalidação guiada por captura de mudanças (CDC) do log do banco. TTLs curtos para dados que mudam com frequência (disponibilidade de lugares), longos para os que raramente mudam (descrição dos locais).
LRU ou LFU?
LRU quando o uso recente prevê o uso futuro e a carga não tem grandes varreduras; LFU (com decaimento) quando a popularidade é estável e você precisa proteger o conjunto quente de tráfego avulso, como um crawler lendo cada página de evento uma vez. Caches modernos em processo usam W-TinyLFU, que combina uma pequena janela de recência com um filtro de admissão por frequência e é robusto aos dois casos. No Redis, allkeys-lfu ou allkeys-lru são as escolhas usuais para um cache puro.
Como você faria o cache sobreviver a um reinício do cluster inteiro sem martelar o banco?
Evite que ele fique frio: persista snapshots e recarregue-os ao iniciar (RDB do Redis), reinicie os nós um a um com as réplicas atendendo nesse meio-tempo, ou aqueça o cache a partir do banco com um job em segundo plano (as N chaves mais acessadas segundo os logs) antes de mandar tráfego. Limite a taxa das faltas que chegam ao banco durante o aquecimento.
Quando você não deveria acrescentar um cache?
Quando a taxa de acerto seria baixa (acesso aleatório uniforme num espaço de chaves enorme), quando os dados precisam ser fortemente consistentes em toda leitura, quando o banco já atende a carga com folga, ou quando a complexidade operacional acrescentada (bugs de invalidação, estouros de manada, um novo modo de falha) custa mais do que a latência que economiza. Meça primeiro (engenharia de desempenho).
10. Rubrica de autoavaliação
| Nível | O que a resposta contém |
|---|---|
| Júnior | Um mapa chave-valor em memória com get/set e TTL; vários nós com hash % N; despejo LRU. Não percebe o que acontece quando N muda. |
| Pleno | Hash consistente com nós virtuais (ou hash slots); escolhas de despejo e TTL; primário/réplica com failover; cache-aside e proteção contra estouro de manada; dimensionamento pela memória. |
| Sênior | Dimensiona por memória e ops/s e liga a taxa de acerto à carga no banco; explica quantitativamente o movimento de chaves e o equilíbrio de carga; perda na replicação assíncrona e split brain com failover por quórum; chaves quentes (L1, replicação da chave) e chaves grandes; opções de roteamento com trade-offs; leases/expiração antecipada; aquecimento e métricas operacionais; sabe quando um cache é a ferramenta errada. |
Relacionados: Cache, Redis, Modos de falha de cache, Particionamento, Hands-on 5 (cache-aside com Redis, taxa de acerto e estouro de manada).
Boas práticas
- Defina
maxmemorye a política de despejo explicitamente —allkeys-lrupara um cache; comnoeviction, um cache cheio vira erros de escrita. - Acrescente jitter a todo TTL — chaves gravadas juntas não podem expirar juntas; o lab 5 acrescenta até 10% (Cache — modos de falha).
- Acompanhe juntos a taxa de acerto, os despejos e a memória por nó — despejos subindo com a taxa de acerto estável significam que o conjunto de trabalho está crescendo até o limite.
- Mantenha o cache opcional no caminho do código — num erro do cache, leia da fonte; o lab 5 registra e conta um
BYPASS(lab 5). - Aqueça as chaves quentes antes de um pico conhecido — a página do evento é carregada às 9h55, não pelo primeiro de 2 M de fãs às 10:00.
Como se conecta
Quais aulas este caso testa? Cada ramo é uma aula cujas ideias o design usa (azul), com os componentes da paleta em que elas aparecem (verde):
flowchart LR
CASE(["Distributed cache"]) --> EST["Estimation<br/>size by memory and ops/s"]:::tech
CASE --> PART["Databases<br/>consistent hashing, virtual nodes"]:::tech
CASE --> EVI["Caching<br/>eviction LRU, LFU, W-TinyLFU, TTL"]:::tech
CASE --> REP["Databases<br/>async replication, failover"]:::tech
CASE --> HOT["Caching<br/>hot keys, big keys, stampedes"]:::tech
CASE --> ROUTE["routing<br/>cluster-aware client"]:::tech
PART -.-> k1["cache (cluster)"]:::comp
REP -.-> k2["sql · nosql<br/>(source of truth)"]:::comp
ROUTE -.-> k3["app"]:::comp
HOT -.-> k4["metrics"]:::comp
classDef comp fill:#1e9e5a22,stroke:#1e9e5a
classDef tech fill:#2f6fed22,stroke:#2f6fed Aulas que ele testa: Caching — padrões · Caching — Redis · Cache — modos de falha · Bancos de dados — particionamento · Bancos de dados — replicação · Estimativas — números · Hands-on 5. Componentes da paleta: cache · app · sql · nosql · metrics. Cenários: read-heavy-cache, sharding.
Referências
- KARGER, D. et al. Consistent Hashing and Random Trees1.
- DECANDIA, G. et al. Dynamo: Amazon's Highly Available Key-value Store2.
- NISHTALA, R. et al. Scaling Memcache at Facebook — leases, pools regionais, mcrouter3.
- EINZIGER, G.; FRIEDMAN, R.; MANES, B. TinyLFU: A Highly Efficient Cache Admission Policy4.
- XU, A. System Design Interview, vol. 1, cap. 5 "Design Consistent Hashing" e cap. 6 "Design a Key-Value Store"5; especificação do Redis Cluster6.
-
KARGER, D. et al. Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web. Proceedings of the 29th ACM STOC, 1997. ↩
-
DECANDIA, G. et al. Dynamo: Amazon's Highly Available Key-value Store. SOSP, 2007. ↩
-
NISHTALA, R. et al. Scaling Memcache at Facebook. USENIX NSDI, 2013. ↩
-
EINZIGER, G.; FRIEDMAN, R.; MANES, B. TinyLFU: A Highly Efficient Cache Admission Policy. ACM Transactions on Storage, 13(4), 2017. ↩
-
XU, A. System Design Interview — An Insider's Guide, vol. 1. Byte Code LLC, 2020. ↩
-
Redis Cluster specification. Documentação do Redis. ↩