"Só dois problemas difíceis em computação: invalidação de cache e nomear coisas"

Caching distribuído e Redis, do cache-aside ao cluster

Cache é a alavanca de performance mais usada e a fonte de bug mais sutil. Esta apostila trata os dois lados: os padrões e as armadilhas de caching (stampede, avalanche, consistência) e o Redis a fundo — estruturas, eviction, rate limiting, locks, filas, Cluster, persistência e operação.

10 móduloscache-aside → write-behindstampede & avalancheRedis Cluster & Sentinelrate limit · locks · streamsexercícios com gabarito
💡 Onde esta apostila se encaixa

O módulo 5 da apostila de NoSQL apresenta Redis; esta aprofunda o uso de Redis e a disciplina de caching como um todo (que vale para qualquer cache: CDN, in-process, HTTP). A parte de consistência dialoga com a Modelagem de Dados NoSQL e a de operação com Observabilidade & SRE.

MÓDULO 01 · BÁSICO

Por que cache existe e onde ele vive

Objetivo: entender o que um cache troca (frescor por velocidade/custo), a hierarquia de caches de um sistema web, e as três métricas que governam tudo.

1.1 O trade-off fundamental

Um cache guarda o resultado de uma operação cara (consulta, cálculo, chamada de API) perto de quem pede, para não repeti-la. Você troca frescor (o dado pode estar desatualizado) por latência (ordens de magnitude menor), carga (o backend recebe menos) e custo (menos CPU, menos chamadas pagas). A pergunta de projeto: quanto de desatualização este dado tolera? — de zero (saldo) a minutos (catálogo) a "para sempre até mudar" (um post publicado).

1.2 A hierarquia de caches

CamadaOndeEscopoTTL típico
Browser cacheno cliente1 usuáriominutos a dias (via Cache-Control)
CDN / edgePoPs perto do usuárioglobal, por objetosegundos a dias
Cache HTTP reversoVarnish/Nginx à frente da apppor rotasegundos a minutos
Cache in-process (L1)memória da instância da app1 instânciasegundos
Cache distribuído (L2)Redis/Memcachedtodas as instânciassegundos a horas
Cache do bancobuffer pool, materialized viewso bancogerido pelo SGBD

Cada camada resolve o pedido mais barato que a de baixo. O erro comum é pular direto para "colocar Redis" sem considerar CDN (para conteúdo público) ou L1 (para dados minúsculos e ultra-quentes).

1.3 As três métricas

💼 Mercado de trabalho

Pergunta de triagem: "o que é hit ratio e qual seria um valor ruim?". Pleno: "onde você colocaria cache num sistema com estas telas?" — a boa resposta percorre a hierarquia (CDN para o estático e o público, HTTP/edge para páginas, Redis para dados de sessão/consulta, L1 para o punhado de chaves quentíssimas) em vez de "põe tudo no Redis".

✏️ Exercício 1 — Escolha a camada

Para cada item, diga a camada de cache mais adequada e o TTL aproximado: (a) o logo e o CSS do site; (b) a página de um produto (muda de preço 1×/dia); (c) o feed personalizado do usuário logado; (d) a tabela de câmbio usada em todo request (atualiza a cada 10 min); (e) o resultado de uma agregação de relatório que leva 8 s e é aberta ~5×/dia.

Gabarito: (a) CDN, TTL longo (dias) com hash no nome do arquivo para invalidar por deploy. (b) CDN/edge ou cache HTTP reverso, TTL de minutos + stale-while-revalidate; purge no evento de mudança de preço. (c) Não cacheia a página inteira (é por usuário); cacheia os componentes compartilhados e o resultado de consultas em Redis com TTL curto (segundos a 1–2 min), chave por usuário. (d) L1 in-process com TTL de ~1 min (dado minúsculo, lido em todo request, tolera 1 min de atraso) + L2 Redis como fonte. (e) Redis (ou tabela materializada) com TTL de algumas horas e, melhor, refresh-ahead/recomputação agendada — 8 s no caminho do usuário é inaceitável mesmo raramente.

MÓDULO 02 · BÁSICO

Redis como cache: estruturas e eviction

Objetivo: usar as estruturas do Redis com intenção (não só GET/SET), e configurar maxmemory + política de expulsão para o Redis se comportar como cache e não estourar.

2.1 As estruturas e para que servem

TipoUso típico em cache/infra
Stringvalor serializado (JSON, protobuf), contador (INCR), flag; SET k v EX 300
Hashobjeto com campos acessados/atualizados isoladamente (perfil, config); economiza memória vs N strings
Listfila simples (LPUSH/BRPOP), timeline recente com LTRIM
Setpertencimento, deduplicação, tags; operações de conjunto
Sorted Set (ZSet)leaderboard, fila de prioridade, janela deslizante de rate limit (score = timestamp)
Streamlog append-only com consumer groups — fila durável com confirmação e replay
Bitmap / Bitfieldpresença/flags por id em pouquíssima memória (usuário ativo hoje)
HyperLogLogcontagem de únicos aproximada (~0,8% de erro) em 12 KB fixos — visitantes únicos
Geoíndice geoespacial (GEOADD/GEOSEARCH) — "lojas perto de mim"

2.2 TTL e expiração

2.3 maxmemory e políticas de eviction

maxmemory 4gb
maxmemory-policy allkeys-lru
PolíticaComportamentoQuando
noevictionrecusa escritas quando cheio (erro)Redis como store/fila — não como cache
allkeys-lruexpulsa a chave menos usada recentementecache genérico — padrão
allkeys-lfuexpulsa a menos frequentemente usadacache com "quentes" muito estáveis; resiste a varreduras
volatile-lru / volatile-ttlsó expulsa chaves com TTL (as com TTL, primeiro as de menor TTL)instância mista cache + dados persistentes
⚠️ noeviction num cache = incidente

Se a política é noeviction e a memória enche, todo SET passa a falhar — e a aplicação, se não trata isso, quebra em cascata. Para um cache, use allkeys-lru/lfu e monitore evicted_keys: eviction constante e alta significa working set > memória (aumente a RAM ou reduza o que se cacheia).

💼 Mercado de trabalho

Cobrado com frequência: "diferença entre allkeys-lru e volatile-lru", "o que acontece quando o Redis enche" (depende da política — e noeviction quebra escritas), "quando usar Hash em vez de várias strings" (campos isolados, economia de memória), "para que serve um Sorted Set" (leaderboard, rate limit por janela).

✏️ Exercício 2 — Estrutura e política

(a) Você precisa contar visitantes únicos por dia de um site com dezenas de milhões de acessos, com erro de ~1% tolerável. Qual estrutura e quanto de memória? (b) Um ranking dos 100 jogadores com maior pontuação, atualizado a cada partida. Qual estrutura e comando para "top 10"? (c) A instância guarda cache e tokens de sessão (que não podem ser expulsos). Qual maxmemory-policy?

Gabarito: (a) HyperLogLog: PFADD visitas:2026-09-01 <userId> por acesso, PFCOUNT para ler — ~12 KB por dia, independente do volume. (b) Sorted Set: ZADD ranking <score> <jogador>; "top 10" = ZREVRANGE ranking 0 9 WITHSCORES. (c) volatile-lru (ou volatile-ttl): só expulsa chaves com TTL — os itens de cache têm TTL e podem sair; as sessões, se gravadas sem TTL (ou com TTL longo e política ttl), ficam protegidas. Melhor ainda: separar em duas instâncias/DBs.

MÓDULO 03 · INTERMEDIÁRIO

Padrões de cache e invalidação

Objetivo: conhecer os cinco padrões de leitura/escrita de cache pelo nome, com seus trade-offs, e as estratégias de invalidação.

3.1 Os padrões

PadrãoComo funcionaTrade-off
Cache-aside (lazy loading)a app lê o cache; no miss, busca no banco e popula o cacheo mais comum; só cacheia o que é pedido; primeiro acesso é lento; risco de stampede
Read-througha app lê sempre da camada de cache, que busca no banco no miss (lógica no cache/lib)código da app mais limpo; acopla ao provedor de cache
Write-throughtoda escrita passa pelo cache, que grava no banco de forma síncronacache sempre fresco; escrita mais lenta; cacheia dados que talvez nunca sejam lidos
Write-behind (write-back)escreve no cache e confirma; o banco é atualizado em lote, assíncronoescrita rapidíssima; risco de perda se o cache cai antes de persistir; complexidade alta
Refresh-aheado cache recarrega proativamente itens quentes antes de expiraremelimina o miss para os quentes; desperdiça recurso se a previsão erra

3.2 Cache-aside, o passo a passo (e onde ele erra)

// leitura
v = redis.get(k)
if v is None:
    v = db.query(...)                // MISS
    redis.set(k, serialize(v), ex=300)
return v

// escrita — INVALIDA, não atualiza o cache
db.update(...)
redis.delete(k)                       // próximo leitor repopula com o valor fresco do banco

3.3 Estratégias de invalidação

EstratégiaComoBom para
TTL purodeixa expirar; aceita janela de stalenessdado que tolera atraso fixo (catálogo, contadores)
Write-invalidatea escrita deleta a(s) chave(s) afetada(s)dado que precisa refletir mudança rápido
Versão / geração de chavea chave inclui um número que muda quando o conjunto muda: user:42:v7:profileinvalidar um grupo inteiro de chaves de uma vez (bump da versão)
Tags / índice reversomanter um Set tag:produto:99 → {chaves} e apagar todas ao mudar o produtoquando uma mudança afeta muitas chaves derivadas
CDC / eventosmudanças no banco viram eventos que invalidam o cachedesacoplar a invalidação do código de escrita; múltiplos consumidores
💡 O truque da geração de chave

Invalidar "todas as chaves de listagem do usuário 42" com KEYS user:42:list:* é proibido em produção (KEYS bloqueia o Redis). Em vez disso, guarde user:42:gen = 7 e componha as chaves como user:42:g7:list:.... Para invalidar tudo, INCR user:42:gen — as chaves antigas ficam órfãs e são expulsas por LRU/TTL. Zero varredura.

💼 Mercado de trabalho

"Explique cache-aside e por que, na escrita, você deleta a chave em vez de atualizá-la" é pergunta clássica. E "como você invalidaria muitas chaves relacionadas sem usar KEYS?" — resposta: versão/geração de chave, ou tags com Set reverso, ou eventos. Citar o perigo do KEYS mostra experiência.

✏️ Exercício 3 — Escolha o padrão

(a) Perfil de usuário, lido muito, escrito pouco, precisa refletir edições em segundos. (b) Contador de visualizações de vídeo — milhares de incrementos/s, leitura tolera minutos de atraso. (c) Preferências de UI — precisa estar sempre certo logo após salvar, escrita e leitura na mesma sessão. Diga o padrão de leitura, o de escrita e a invalidação.

Gabarito: (a) Cache-aside na leitura; na escrita, db.update + redis.del(user:X:profile); TTL de ~10 min como rede. (b) Write-behind de fato: INCR no Redis a cada view (rápido, absorve o pico), um job a cada minuto faz GETDEL por vídeo e soma no banco; leitura pode servir do Redis (fresco) ou do banco (atrasado). (c) Write-through ou write-invalidate com read-your-writes: ao salvar, atualiza o banco e seta o cache com o novo valor (aqui atualizar é aceitável porque a escrita é do próprio usuário, sem concorrência típica), ou deleta e força a próxima leitura a ir ao banco; TTL curto.

MÓDULO 04 · INTERMEDIÁRIO

Os problemas clássicos

Objetivo: reconhecer e mitigar os cinco modos de falha que todo sistema com cache encontra em escala — e que caem em toda entrevista de system design.

4.1 Cache stampede / dogpile / thundering herd

Sintoma: uma chave quente expira; centenas de requisições dão miss ao mesmo tempo e todas vão ao banco recomputar o mesmo valor, saturando-o.

4.2 Cache avalanche

Sintoma: muitas chaves expiram ao mesmo tempo (populadas juntas num deploy, com TTL idêntico) — ou o Redis inteiro cai — e a carga desaba no banco de uma vez.

4.3 Cache penetration

Sintoma: requisições por chaves que não existem no banco (ids inválidos, ataque) sempre dão miss e sempre batem no banco — o cache não protege.

4.4 Hot key

Sintoma: uma única chave (o post viral, o produto da Black Friday) recebe tantas leituras que satura o nó do Redis que a hospeda (num Cluster, um slot num nó).

4.5 Big key

Sintoma: uma chave gigante (um Hash com milhões de campos, uma List de centenas de MB) — operações sobre ela bloqueiam o Redis (single-thread), a replicação engasga, e a expulsão dela causa um stall.

💡 A tabela mental para a entrevista

stampede = uma chave quente expira → lock / SWR / early recompute. avalanche = muitas expiram juntas / Redis cai → jitter no TTL / warm-up / degradação. penetration = chave inexistente → negative cache / bloom. hot key = leitura concentrada → L1 / réplicas de chave. big key = chave enorme → quebrar / SCAN.

💼 Mercado de trabalho

Estes cinco são o conteúdo de cache em system design de nível pleno/sênior. Saber diferenciar stampede de avalanche (uma chave vs muitas), e ter uma mitigação nomeada para cada, é o que se espera. "Eu poria jitter no TTL e um lock de single-flight" numa pergunta sobre pico pós-deploy é resposta direta.

✏️ Exercício 4 — Diagnostique

Toda segunda às 9h, logo após o deploy de sexta, o p99 do serviço de catálogo dispara, o banco satura e o hit ratio do Redis cai de 96% para 55% por ~3 minutos. Além disso, um produto específico em promoção deixa um nó do Redis Cluster a 100% de CPU. Nomeie os dois problemas e proponha as correções.

Gabarito: (1) Avalanche: o deploy de sexta repopulou o cache com TTL uniforme (ex.: 72 h), que vence em bloco na segunda de manhã; a recomputação simultânea satura o banco. Correções: jitter no TTL (base 72 h ± 20%), warm-up das chaves quentes no pipeline de deploy, e um lock de single-flight para o caso de várias instâncias recomputarem a mesma chave. (2) Hot key: o produto em promoção concentra leitura num slot/nó. Correções: L1 in-process (TTL 2–5 s) para esse produto (e para o "top N" em geral), e/ou replicar a chave em produto:99:r0..r9 com o cliente sorteando; monitorar com --hotkeys.

MÓDULO 05 · INTERMEDIÁRIO → AVANÇADO

Consistência cache + banco

Objetivo: enxergar as corridas entre "atualizar o banco" e "mexer no cache", e escolher a estratégia que dá a consistência que o dado exige.

5.1 Por que "atualizar os dois" não é atômico

Cache e banco são sistemas separados; não há transação entre eles. Qualquer sequência de "escreve no banco" + "mexe no cache" tem uma janela onde um crash, um timeout ou uma reordenação deixa os dois divergentes. O objetivo não é eliminar a divergência (impossível), é torná-la breve e auto-corrigível.

5.2 A corrida clássica da invalidação

// Thread A (leitura, deu miss)      Thread B (escrita)
A: lê do banco  → v_antigo
                                     B: escreve no banco → v_novo
                                     B: DEL cache[k]
A: SET cache[k] = v_antigo           // ← cache agora tem valor velho, sem TTL de socorro próximo

5.3 Invalidate vs update na escrita

AbordagemPrósContras
Write-invalidate (DEL na escrita)simples; o próximo leitor traz o valor canônico do banco; menos corridaso primeiro leitor após a escrita paga um miss
Write-update (SET com o novo valor na escrita)sem miss depois da escritaduas escritas concorrentes podem inverter a ordem; precisa de versão/CAS para ser seguro

Padrão de mercado: write-invalidate por default; write-update só quando o miss pós-escrita é caro e você adiciona controle de versão.

5.4 Invalidação por CDC avançado

Em vez de o código de escrita lembrar de invalidar todas as chaves derivadas, um consumidor de Change Data Capture (Debezium lendo o WAL/oplog) recebe cada mudança e invalida as chaves afetadas. Vantagens: desacopla (quem escreve não precisa conhecer o cache), cobre todas as fontes de escrita (incluindo migrações e jobs), e é uma trilha auditável. Custo: mais uma peça de infra e consistência eventual (o atraso do pipeline). Liga com o módulo 9 da apostila de NoSQL.

⚠️ Não existe cache fortemente consistente barato

Se um dado não pode estar desatualizado nem por 1 segundo (saldo, estoque no checkout, permissões), a resposta honesta muitas vezes é não cacheá-lo, ou cachear com TTL de poucos segundos e ler a fonte no caminho crítico. "Consistência forte" com cache exige invalidação síncrona e coordenada que anula boa parte do ganho de performance.

💼 Mercado de trabalho

Pergunta de sênior: "desenhe a race condition entre um leitor que deu miss e um escritor concorrente, e diga como mitigar". A resposta completa cita: TTL curto como piso, delayed double delete, versão monotônica com CAS, e o reconhecimento de que dado crítico talvez não deva ser cacheado.

✏️ Exercício 5 — Escolha a estratégia de consistência

(a) Nome e foto de exibição do usuário, mostrados em todo lugar. (b) Saldo de créditos usado para autorizar uma compra. (c) Configuração de feature flags lida em todo request. Para cada um: cachear? qual TTL? invalidate ou update? algum mecanismo extra?

Gabarito: (a) Cachear, TTL 5–15 min, write-invalidate na edição; tolera minutos de atraso, corrida é inofensiva. Se quiser instantâneo, publicar um evento de invalidação. (b) Não cachear o saldo autoritativo para autorização — ler a fonte (ou um contador transacional) no caminho do checkout. Pode-se cachear um saldo "para exibição" com TTL de segundos e rótulo "aproximado". (c) Cachear em L1 in-process com TTL de 30–60 s (dado minúsculo, lido em todo request) + L2 Redis; invalidação por pub/sub quando uma flag muda, para propagar em segundos a todas as instâncias; nunca update cego, e sim recarregar o conjunto do provedor de flags.

MÓDULO 06 · AVANÇADO

Redis além do cache

Objetivo: usar o Redis para os problemas de infraestrutura que ele resolve melhor que qualquer alternativa — rate limiting, locks, filas, pub/sub — com atenção aos perigos de cada um.

6.1 Rate limiting

// Token bucket atômico em Lua (evita corrida entre GET e SET)
// KEYS[1]=bucket  ARGV: taxa, capacidade, agora, custo
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(b[1]) or tonumber(ARGV[2])
local ts = tonumber(b[2]) or tonumber(ARGV[3])
tokens = math.min(tonumber(ARGV[2]), tokens + (ARGV[3]-ts)*ARGV[1])
if tokens < tonumber(ARGV[4]) then return 0 end
redis.call('HMSET', KEYS[1], 'tokens', tokens-ARGV[4], 'ts', ARGV[3])
redis.call('EXPIRE', KEYS[1], 3600)
return 1

6.2 Locks distribuídos com ressalvas

SET lock:recurso <token-aleatório> NX PX 30000     // adquire: só se não existe, com expiração
// libera com Lua: só apaga se o token for meu (senão apago o lock de outro)
if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end

6.3 Filas: Lists vs Streams

List (LPUSH/BRPOP)Stream (XADD/XREADGROUP)
Entregauma vez, sem confirmação (some ao ler)consumer groups com XACK; mensagem não confirmada volta (PEL)
Histórico / replaynãosim (log append-only, XRANGE)
Vários consumidorescompetição simplesgrupos com partição por consumidor + fan-out
Usofila de trabalho simples, best-effortfila durável, event sourcing leve, pipelines

Redis não substitui Kafka/RabbitMQ para garantias fortes e volume altíssimo, mas Streams cobrem muito caso de fila sem trazer outra peça de infra.

6.4 Pub/Sub e Keyspace Notifications

6.5 Outros usos consagrados

💼 Mercado de trabalho

"Para que você usa Redis além de cache?" espera uma lista com um caso que você implementou: rate limiting (e qual algoritmo), lock distribuído (e por que token único + TTL, e a ressalva do Redlock), fila com Streams vs List, pub/sub para invalidação, leaderboards. Mencionar fencing tokens ao falar de lock é um diferencial forte.

✏️ Exercício 6 — Lock seguro?

Um job de faturamento roda em cron em 3 instâncias; só uma deve executar por ciclo. O time usa SET lock:faturamento 1 NX EX 600 e, no fim, DEL lock:faturamento. Aponte os dois bugs e diga quando esse esquema ainda assim é aceitável.

Gabarito: Bug 1: valor fixo 1 em vez de token único — se o job da instância A demora mais que 600 s, o lock expira, a instância B adquire, e quando A termina ela dá DEL no lock de B, deixando C entrar: dois donos. Correção: valor = token aleatório por execução, e liberar com Lua que só apaga se o token bate. Bug 2: TTL fixo de 600 s sem renovação — se o job legitimamente passa de 600 s, perde o lock no meio. Correção: heartbeat que estende o TTL enquanto o job vive, ou dimensionar o TTL com folga e tornar o job idempotente. Aceitável assim mesmo quando o job é idempotente e rodar duas vezes só desperdiça recurso (não corrompe dado) — é um lock "de eficiência", não "de correção". Para correção, fencing token + verificação no recurso, ou etcd/ZooKeeper.

MÓDULO 07 · AVANÇADO

Escala e operação do Redis

Objetivo: entender replicação, Sentinel, Cluster, persistência e o modelo de execução do Redis o suficiente para operá-lo — e para responder por que ele é rápido.

7.1 Por que o Redis é rápido (e onde isso morde)

7.2 Disponibilidade: réplica + Sentinel

7.3 Escala horizontal: Redis Cluster

7.4 Persistência: RDB e AOF

RDB (snapshot)AOF (append-only file)
O quêdump binário periódico do datasetlog de cada comando de escrita
Perda em crashtudo desde o último snapshot (minutos)everysec: ~1 s; always: ~0 (lento)
Restartrápidomais lento (reexecuta o log); mitigado por rewrite/compactação
Usobackup, réplica inicialdurabilidade melhor; combine os dois

Para cache puro, muitos rodam sem persistência (o dado é reconstruível do banco) — reduz latência e risco de fork pesado. Para Redis como store/fila, AOF everysec + RDB.

7.5 Memória e latência: o que monitorar

💼 Mercado de trabalho

Vaga de backend sênior / SRE cobra: por que o Redis é single-thread e o que isso implica (comando lento bloqueia tudo), diferença Sentinel vs Cluster, os 16384 slots e hash tags, RDB vs AOF, e as métricas de operação (fragmentação, evicted_keys, hit ratio, slowlog). "Rodo o cache sem persistência porque o dado é reconstruível" é uma resposta que mostra que você pensou no trade-off.

✏️ Exercício 7 — Cluster e multi-chave

(a) Sua app faz MGET user:1:name user:1:tier user:1:plan e, ao migrar para Redis Cluster, passa a receber erro de "CROSSSLOT". Por quê e como resolver sem mudar a lógica? (b) O mem_fragmentation_ratio está em 1,8 e a latência p99 subiu. O que investigar? (c) É um cache puro; vale ligar AOF always?

Gabarito: (a) As três chaves caem em slots diferentes, e operações multi-chave no Cluster exigem mesmo slot. Solução: hash tag — {user:1}:name, {user:1}:tier, {user:1}:plan forçam o mesmo slot (só o conteúdo de {} entra no CRC16), e o MGET volta a funcionar. (b) Fragmentação de 1,8 significa que o processo reserva muito mais RAM que os dados usam — investigar: padrão de escrita com muitos tamanhos diferentes de valor, big keys que foram removidas, falta de activedefrag; verificar se há swap (fatal), rodar MEMORY DOCTOR, considerar activedefrag yes e reiniciar réplicas em rotação para compactar. (c) Não — always faz fsync a cada escrita, derruba o throughput, e para um cache o dado é reconstruível do banco. Rodar sem persistência (ou só RDB esparso para acelerar warm-up) é o certo.

MÓDULO 08 · AVANÇADO

Cache em camadas e na borda

Objetivo: combinar L1 in-process + L2 Redis com coerência entre instâncias, e usar o cache HTTP/CDN corretamente (Cache-Control, ETag, stale-while-revalidate).

8.1 L1 (in-process) + L2 (Redis)

8.2 O problema da coerência entre L1s

Cada instância tem seu próprio L1. Quando um dado muda, invalidar o L2 (Redis) não invalida os N L1s. Estratégias:

8.3 Cache HTTP e na borda

CabeçalhoEfeito
Cache-Control: public, max-age=60qualquer cache (browser, CDN) pode guardar por 60 s
Cache-Control: private, no-cachesó o browser; revalida sempre antes de usar
s-maxage=300TTL só para caches compartilhados (CDN), separado do browser
stale-while-revalidate=30serve o conteúdo vencido por até 30 s enquanto revalida em background — sem espera
stale-if-error=600serve o vencido se a origem está fora — resiliência
ETag / If-None-Matchrevalidação barata: origem responde 304 Not Modified sem corpo
Vary: Accept-Encodingcacheia variantes por cabeçalho — cuidado, Vary demais destrói o hit ratio
💼 Mercado de trabalho

Full-stack e frontend sênior: saber a diferença max-age vs s-maxage, o valor de stale-while-revalidate, como ETag economiza banda, e o perigo do Vary. Backend distribuído: L1+L2 com coerência por pub/sub ou RESP3 tracking, e por que o L1 é a defesa contra hot key.

✏️ Exercício 8 — Camadas para uma home

A home de um portal de notícias tem: cabeçalho/menu (muda 1×/semana), a manchete principal (pode mudar a qualquer momento, precisa propagar em ~1 min), um bloco "mais lidas" (recalculado a cada 5 min), e uma faixa de anúncios personalizada por usuário. Descreva a estratégia de cache camada por camada.

Gabarito: Cabeçalho/menu: fragmento com Cache-Control: public, s-maxage=86400, stale-while-revalidate=3600 na CDN; purge no deploy de conteúdo. Manchete: fragmento com s-maxage=60, stale-while-revalidate=30 + purge por evento quando a editoria troca a manchete (propaga em segundos, o SWR cobre a janela). "Mais lidas": recalculado por job a cada 5 min, gravado no Redis; o fragmento tem s-maxage=300 com jitter. Faixa de anúncios: Cache-Control: private, no-store (por usuário) — montada na origem ou na edge com um KV; nunca em cache compartilhado. A página é composta desses fragmentos (ESI/edge) para que cada um tenha seu TTL. L1 in-process nas APIs internas que servem "mais lidas" e manchete, TTL 2–5 s, com pub/sub de invalidação para a manchete.

MÓDULO 09 · MUITO AVANÇADO

Casos especiais e alternativas

Objetivo: cache de LLM (exato e semântico), Redis como banco primário (quando e riscos), e o panorama de alternativas — Memcached, Dragonfly, Valkey, KeyDB — com o ângulo de custo.

9.1 Cache de LLM tema em alta

TipoComoCuidado
Exatochave = hash do prompt normalizado (+ modelo + parâmetros); valor = respostasó acerta prompts idênticos; normalize (trim, lowercase onde couber); inclua a versão do sistema/prompt na chave
Semânticoembedding do prompt; no miss exato, busca vetorial por similaridade > limiar e reusa a respostalimiar mal calibrado devolve resposta errada com confiança; use limiar alto e valide; não use para prompts com contexto único (dados do usuário)
Prefixo / KV de contextoreaproveitar o processamento de um prefixo grande e estável (system prompt, documentos) — via prompt caching do provedoré do lado do provedor; estruture o prompt com o estável primeiro

Redis serve os três: string para o exato, Redis com busca vetorial (RediSearch/Redis Query Engine) para o semântico. Ganho típico: latência e custo de tokens caem muito em fluxos com repetição (FAQ, classificação, prompts de template). Ver a apostila de RAG e a de LLMOps.

9.2 Redis como banco primário — quando e riscos

9.3 O panorama de alternativas

OpçãoPerfil
Memcachedcache puro, multi-thread, sem estruturas ricas, sem persistência; simples e previsível para "string → string" em escala. Sem replicação nativa.
Valkeyfork do Redis mantido pela Linux Foundation após a mudança de licença do Redis (2024); compatível, multi-thread em evolução; o que muitas nuvens adotaram como padrão.
Redis (Redis Inc.)o original; licença dupla (RSALv2/SSPL) desde 2024, com módulos (Query Engine, JSON, TimeSeries, Bloom) e Redis Enterprise (Active-Active/CRDT, tiered storage).
Dragonflyreimplementação compatível com o protocolo, multi-thread, foco em throughput vertical e menos nós; boa quando um só nó Redis não aguenta e você quer evitar Cluster.
KeyDBfork multi-thread do Redis (mais antigo); nicho.

9.4 O ângulo de custo

💼 Mercado de trabalho

Tópicos que aparecem em 2026: cache de LLM (exato vs semântico e o risco do limiar), a existência do Valkey e por que ele surgiu (mudança de licença do Redis), quando Redis pode ser primary store (e a config noeviction + AOF + o risco do failover assíncrono), e a análise de custo dos dois lados. Não precisa decorar — precisa saber que existem e o trade-off de cada um.

✏️ Exercício 9 — Cache semântico com juízo

Um time quer cache semântico para um assistente de suporte: no miss exato, se houver uma pergunta anterior com similaridade > 0,80, reusa a resposta. Aponte os riscos e proponha salvaguardas. Onde esse cache não deve ser aplicado?

Gabarito: Riscos: 0,80 é baixo — "como cancelo minha assinatura?" e "como faço upgrade da assinatura?" podem passar de 0,80 e devolver a resposta errada com confiança; respostas dependem de contexto que mudou (política atual, status da conta); a resposta cacheada pode ter ficado desatualizada. Salvaguardas: limiar bem mais alto (0,92–0,95) calibrado contra um conjunto rotulado; TTL no cache semântico (respostas de suporte envelhecem); um passo de verificação leve ("esta resposta responde a esta pergunta?") antes de servir; nunca cachear respostas que incluíram dados específicos da conta/usuário no contexto; registrar hits semânticos para auditoria e medir a taxa de reclamação nesses hits. Não aplicar: qualquer pergunta cuja resposta dependa do estado atual da conta, de valores/preços, de disponibilidade, ou de qualquer coisa personalizada — só para conhecimento genérico e estável.

MÓDULO 10 · CARREIRA

Mercado de trabalho: roadmap, portfólio e entrevistas

Objetivo: caching é assunto garantido em entrevista de backend e system design — o que estudar, o que construir, e as perguntas com a resposta que aprova.

10.1 Onde cai

PapelO que cobram
Backend plenocache-aside, invalidar vs atualizar, TTL + jitter, estruturas do Redis, rate limiting com INCR/ZSet
Backend sênioros 5 problemas (stampede/avalanche/penetration/hot key/big key), consistência cache+DB e as corridas, lock distribuído e a ressalva do Redlock, L1+L2
System Designonde colocar cache na hierarquia, dimensionar (hit ratio, working set, memória), degradação quando o cache cai
SRE / InfraSentinel vs Cluster, slots + hash tags, RDB/AOF, métricas (fragmentação, evicted_keys, slowlog), capacity
IA Engineeringcache de LLM exato e semântico, feature store online, cache de embeddings

10.2 Roadmap (4 semanas)

10.3 Banco de perguntas (com a resposta que aprova)

Pleno — "Explique cache-aside e por que deletar em vez de atualizar na escrita."

Cache-aside: a app lê o cache; no miss, busca no banco e popula. Na escrita, atualiza o banco e deleta a chave. Deletar evita a corrida em que duas escritas concorrentes deixam no cache o valor da que terminou por último (que pode ser o antigo); o próximo leitor repopula com o valor canônico do banco. Sempre com TTL como rede de segurança contra invalidações perdidas.

Pleno/Sênior — "Diferença entre cache stampede e cache avalanche?"

Stampede: uma chave quente expira e muitas requisições recomputam o mesmo valor ao mesmo tempo, saturando o banco — mitiga com lock de single-flight, stale-while-revalidate, ou recomputação probabilística antes de expirar. Avalanche: muitas chaves expiram juntas (TTL uniforme, populadas no mesmo deploy) ou o Redis inteiro cai, e a carga desaba de uma vez — mitiga com jitter no TTL, warm-up pós-deploy e degradação graciosa (circuit breaker no banco).

Sênior — "Como você garante consistência entre o cache e o banco?"

Não se garante consistência forte com cache barato — se busca minimizar a janela e torná-la auto-corrigível. Write-invalidate por padrão; TTL curto como piso; para a corrida leitor-com-miss × escritor concorrente, delayed double delete ou versão monotônica com CAS (Lua) que rejeita SET de versão menor. Para desacoplar a invalidação, um consumidor de CDC invalida as chaves. Dado que não pode estar stale nem por 1 s (saldo, permissões) não deve ser cacheado, ou só com TTL de poucos segundos lendo a fonte no caminho crítico.

Sênior — "Redis é single-thread. Isso é um problema?"

É uma escolha: sem locks, sem context switch, latência previsível, e simplicidade. O custo é que um comando lento bloqueia todos os outros — daí a regra de nunca rodar KEYS, SMEMBERS/HGETALL em coleção enorme, ou Lua pesada no caminho quente. Desde a 6.0 há I/O threads para o tráfego de rede, mas o processamento continua serial. Para throughput além de um núcleo, escala-se com Cluster (mais nós) ou usa-se um fork multi-thread (Valkey/Dragonfly).

Sênior — "Você usaria o lock distribuído do Redis para garantir exclusão mútua?"

Para eficiência (evitar trabalho duplicado, e tudo bem se raramente dois rodarem), sim: SET key token NX PX ttl + liberação com Lua verificando o token + heartbeat se o job for longo. Para correção (nunca dois donos, sob risco de corromper dado), não confio só nisso — Redlock tem a crítica do Kleppmann (pausas de GC, relógios) e a resposta do Antirez; a forma segura é fencing token (o lock devolve um número crescente e o recurso rejeita operações com token menor) ou um sistema de consenso de verdade (etcd/ZooKeeper).

10.4 Projetos de portfólio

  1. Laboratório dos 5 problemas: uma API + Redis + k6, com um branch por problema (stampede, avalanche, penetration, hot key, big key) mostrando o sintoma no gráfico de carga e a correção. README com os números antes/depois. Extremamente convincente.
  2. Toolkit de Redis: rate limiter (token bucket em Lua), lock com fencing token, fila com Streams + DLQ, tudo com testes de concorrência.
  3. Cache de LLM: exato + semântico (com limiar calibrado e um conjunto de avaliação medindo falso-reuso), sobre um assistente simples, comparando latência e custo de tokens com e sem cache.

10.5 Fontes

🏁 Síntese final

Três ideias sustentam a apostila: (1) cache troca frescor por velocidade/carga/custo — a decisão é quanto de staleness cada dado tolera, e a resposta define TTL, padrão e invalidação; (2) em escala, todo cache encontra os mesmos cinco problemas (stampede, avalanche, penetration, hot key, big key) e cada um tem uma mitigação nomeada; (3) não existe cache fortemente consistente barato — minimize a janela, ponha TTL como piso, e não cacheie o que não pode estar velho. O Redis é a ferramenta; a disciplina de caching é o que evita o bug.