Cache é uma aposta: que recomputar ou buscar novamente alguma parte dos dados é caro o suficiente, e que os dados mudam devagar o suficiente, que vale a pena armazenar uma cópia em algum lugar mais rápido e arriscar que essa cópia fique desatualizada.
Toda decisão de cache em um serviço Node - o que cachear, por quanto tempo, em processo ou no Redis, cache-aside ou write-through - é na verdade uma decisão sobre quanta desatualização de risco é aceitável em troca de quanta velocidade e custo são economizados.
Noções Básicas de Cache cobre os padrões concretos (cache-aside, estratégia TTL, prevenção de stampede); esta página é sobre o trade-off por trás de todos eles, e especificamente o que o Redis adiciona que um cache em processo não pode.
Um cache armazena uma cópia mais barata de ler de dados que são caros de computar ou buscar, aceitando um risco limitado de que a cópia esteja desatualizada em troca de menor latência e menor carga na origem.
Por que Importa: Todo cache é uma segunda fonte de verdade que pode discordar silenciosamente da real - entender quando e por que ele pode estar errado é o que separa um cache que ajuda de um que serve dados incorretos aos usuários.
Conceitos Chave:origem, cache-aside, TTL (time to live - tempo de vida), invalidação, coerência de cache, stampede.
Quando Usar: Dados que são lidos muito mais frequentemente do que mudam, computações ou agregações caras, resultados de chamadas externas lentas e qualquer origem (banco de dados, API) cuja carga precise ser protegida de picos de tráfego de leitura.
Limitações / Trade-offs: Um cache adiciona um modo de falha que a origem não tinha (desatualização, ou o próprio cache caindo); ele nunca torna as escritas mais rápidas, e adiciona uma superfície operacional real - expurgo, limites de memória e um segundo sistema para manter disponível.
Tópicos Relacionados: pooling de conexões e carga do banco de dados, travamento distribuído, armazenamento de sessão, cabeçalhos cache-control e CDNs.
Todo cache fica entre um leitor e uma origem - a fonte real da verdade, seja um banco de dados, uma API externa ou uma computação cara em processo - e seu propósito inteiro é responder a leituras sem incomodar essa origem toda vez.
O mecanismo que quase todo serviço Node usa é cache-aside: o código do aplicativo verifica o cache primeiro, e somente em caso de falha ele vai até a origem, armazenando o resultado de volta no cache antes de retorná-lo - o cache nunca fala com a origem sozinho, o aplicativo sempre media.
const cached = await redis.get(key);if (cached) return JSON.parse(cached); // cache hit - origem nunca tocadaconst value = await loadFromOrigin(); // cache miss - vai para a fonte da verdadeawait redis.set(key, JSON.stringify(value), "EX", 300); // armazena para a próxima vezreturn value;
No momento em que um valor é copiado para um cache, ele se torna um segundo registro da existência desse dado independentemente do primeiro - o que significa que ele agora pode estar errado de uma forma que a origem, sendo a fonte da verdade, estruturalmente não pode.
Uma analogia simples: um cache é como um post-it com um número de telefone copiado da sua agenda - rápido de olhar, mas se o número real mudar e ninguém atualizar o post-it, você ligará confiantemente para um número que não funciona mais.
TTL (time to live) é o instrumento contundente que todo cache usa para limitar esse risco sem precisar saber exatamente quando um valor mudou: em vez de tentar detectar toda escrita na origem, um valor em cache simplesmente expira após uma janela fixa, garantindo que a desatualização nunca exceda essa janela, mesmo que nada mais diga ao cache para atualizar.
Invalidação - remover ou atualizar ativamente um valor em cache no momento em que sua origem muda, em vez de esperar um TTL expirar - é a parte difícil do cache, e é difícil por uma razão estrutural: o código que escreve na origem e o código que lê do cache muitas vezes estão em lugares diferentes, às vezes serviços inteiros diferentes, então nada conecta automaticamente "os dados mudaram" a "a cópia em cache está agora errada."
A famosa frase sobre isso ("existem apenas duas coisas difíceis na ciência da computação: invalidação de cache e nomeação de coisas") é uma piada sobre um problema real: um cache sem invalidação só é tão atual quanto seu TTL permite, e um cache com invalidação imperfeita pode servir dados desatualizados indefinidamente se algum caminho de escrita esquecer de limpar a chave certa.
O papel específico do Redis nesse cenário é que ele é um cache compartilhado, fora do processo, em vez de um em processo - e essa distinção muda completamente o tipo de problema de correção com o qual você está lidando.
Sem Redis (cache em processo): Com Redis (cache compartilhado):
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│instância A│ │instância B│ │instância A│ │instância B│
│ cache: {}│ │ cache: {}│ └────┬────┘ └────┬────┘
└─────────┘ └─────────┘ └──────┬──────┘
valores diferentes possíveis │
para a mesma chave entre instâncias ┌────────┐
│ Redis │ um valor compartilhado
└────────┘ por chave
Um cache em processo (em memória) é rápido - sem salto de rede - mas cada instância do servidor mantém sua própria cópia independente, então duas instâncias podem discordar sobre o valor da mesma chave no mesmo momento, e uma escrita em uma instância não pode invalidar a cópia que está na memória de outra instância.
O Redis move o cache para fora do processo inteiramente: cada instância lê e escreve no mesmo armazenamento compartilhado pela rede, então há exatamente um valor em cache por chave em toda a frota - ao custo de um round trip de rede por acesso ao cache, e uma nova dependência que por si só precisa permanecer disponível.
Esse custo de rede também é o motivo pelo qual a serialização importa mais com Redis do que com um cache em processo: valores precisam ser transformados em um payload de string ou byte (geralmente JSON) para cruzar a rede e voltar, o que é um custo real e mensurável de CPU e largura de banda que escala com o quão grande e quão frequentemente acessado um objeto em cache é.
Cache stampede é o que acontece quando uma chave popular expira e muitas requisições concorrentes falham ao mesmo tempo, todas correndo para a mesma chamada cara à origem simultaneamente - o exato pico de tráfego que o cache existia para prevenir, reproduzido momentaneamente em sua totalidade no instante em que o cache não pode ajudar.
As defesas padrão são um lock de voo único (uma requisição repopula o cache enquanto outras esperam ou servem o valor anterior por mais um tempo) ou TTLs com jitter (aleatorizando a expiração ligeiramente por chave para que muitas chaves definidas no mesmo momento não expirem no mesmo instante) - Travamentos Distribuídos cobre as mecânicas de coordenação que um lock de voo único realmente precisa.
Coerência de cache - a garantia de que todos os leitores veem o mesmo valor ao mesmo tempo - é fundamentalmente mais fraca com um cache do que com a própria origem, e isso não é um bug a ser corrigido tanto quanto uma propriedade a ser raciocinada explicitamente: um cache com um TTL de 5 minutos está prometendo no máximo 5 minutos de desatualização, não prometendo atualização, e todo consumidor desses dados em cache precisa ser capaz de tolerar essa janela.
É aqui também que o cache se conecta diretamente à carga do banco de dados: um cache bem posicionado absorve o tráfego de leitura que de outra forma atingiria um pool de conexões com um teto de concorrência rígido, o que faz parte do motivo pelo qual o cache e o dimensionamento do pool de conexões são geralmente ajustados juntos em vez de independentemente - um cache que protege o banco de dados muda o que "capacidade de pool suficiente" significa.
Finalmente, um cache deve sempre degradar, não cascatear - se o Redis ficar indisponível, o comportamento correto é voltar para a origem (com um aviso registrado), não retornar erros aos usuários; a queda de um cache deve tornar um serviço mais lento, nunca fazê-lo falhar completamente, já que a origem era a verdadeira fonte da verdade o tempo todo.
Abordagem
Força
Fraqueza
Melhor Ajuste
Cache em processo (em memória)
Sem salto de rede, mais simples de adicionar
Inconsistente entre instâncias; perdido na reinicialização
Serviços de instância única, tabelas de consulta muito quentes/pequenas
Redis (cache compartilhado)
Um valor consistente entre todas as instâncias; sobrevive a reinicializações de instância
Round trip de rede por acesso; uma nova dependência para manter disponível
Implantações multi-instância, estado de sessão/limite de taxa compartilhado
Cache-Control HTTP / CDN
Nenhum envolvimento do servidor de origem para respostas em cache
Adequado apenas para dados públicos e não personalizados
Ativos estáticos, respostas de API públicas
Cache write-through
Cache e origem atualizados juntos - menor risco de desatualização
Escritas pagam o custo de atualizar ambos os sistemas
Dados onde a desatualização é cara e o volume de escrita é gerenciável
"O cache também acelera as escritas." O cache só acelera leituras que atingem o cache - as escritas ainda precisam chegar à origem, e uma estratégia write-through adiciona uma atualização de cache em cima disso, não em vez disso.
"Um TTL mais longo é sempre mais seguro do que um mais curto." Um TTL mais longo reduz a carga na origem, mas amplia a janela de desatualização - o TTL "seguro" depende inteiramente de quão tolerante esse dado específico é de estar errado por tanto tempo, não de uma regra geral.
"Redis e um cache em processo são intercambiáveis - Redis é apenas a versão 'de produção'." Eles resolvem problemas diferentes: um cache em processo não tem custo de rede, mas não tem consistência entre instâncias; Redis tem custo de rede, mas dá a cada instância a mesma visão de uma chave. Trocar um pelo outro muda o comportamento real, não apenas o desempenho.
"Se o cache estiver errado, isso é um bug de cache." Um cache desatualizado dentro de sua janela de TTL é o cache funcionando como projetado, não um bug - o bug real é esperar que um cache garanta uma atualização que ele nunca prometeu.
"Invalidação de cache significa apenas chamar delete na chave após uma escrita." Esse é o caso fácil - a parte difícil é garantir que cada caminho de código que muda a origem também saiba quais chaves de cache dependem desses dados, o que se torna genuinamente difícil quando dados relacionados abrangem várias chaves ou serviços.
Pelo que um cache está realmente trocando em troca de velocidade?
Garantias de correção - um valor em cache pode estar desatualizado em relação à origem pelo tempo que seu TTL permitir, ou até que algo o invalide explicitamente. A troca é risco de desatualização limitado em troca de menor latência e menor carga na origem.
O que "cache-aside" significa, mecanicamente?
O código do aplicativo verifica o cache primeiro; em caso de falha, ele busca na origem ele mesmo e escreve o resultado de volta no cache antes de retorná-lo. O cache nunca fala diretamente com a origem - o aplicativo sempre media em ambas as direções.
Por que o Redis é diferente de simplesmente cachear dados em um objeto JavaScript na memória?
Um objeto em memória é local a um processo - cada instância do servidor tem sua própria cópia independente que pode discordar das outras. Redis é um serviço separado e compartilhado que todas as instâncias leem e escrevem pela rede, então há um valor consistente por chave em toda a frota.
Por que a invalidação de cache é considerada difícil?
Porque o código que muda a origem e o código que lê o cache muitas vezes estão distantes - nada conecta automaticamente "estes dados acabaram de mudar" a "limpar esta chave de cache", então mantê-los sincronizados depende de cada caminho de escrita lembrar de invalidar as chaves certas.
O que é um cache stampede, e por que ele acontece exatamente quando uma chave expira?
É quando uma chave de cache popular expira e muitas requisições concorrentes falham no mesmo instante, todas atingindo a origem simultaneamente para repopulá-la - reproduzindo exatamente o pico de carga que o cache deveria prevenir, por uma breve janela logo na expiração.
Um serviço ainda deve funcionar se o Redis cair?
Sim - o padrão correto é voltar para a origem (com a falha registrada) para que o serviço fique mais lento, não quebrado. A queda de um cache nunca deve ser capaz de tirar do ar um caminho de leitura que, de outra forma, funcionaria com a origem como base.
Usar um TTL menor é sempre a escolha "correta" para evitar dados desatualizados?
Não - TTL é um dial de trade-off, não uma configuração de correção com uma única resposta certa. TTLs menores reduzem a desatualização, mas aumentam a carga na origem; o valor correto depende de quanta desatualização esses dados específicos podem tolerar.
O cache reduz a pressão no pool de conexões do banco de dados?
Sim, quando bem posicionado - leituras servidas do cache nunca chegam ao pool, o que explica por que o posicionamento do cache e o dimensionamento do pool de conexões são frequentemente ajustados juntos em vez de tratados como preocupações não relacionadas.
Por que o custo de serialização importa mais com Redis do que com um cache em processo?
Valores do Redis viajam por uma conexão de rede, então eles precisam ser serializados (geralmente para JSON) e desserializados a cada acesso - um custo real de CPU e largura de banda que uma referência de objeto em processo nunca paga, pois já está na memória do mesmo processo.
Qual a diferença entre cacheamento e o cache HTTP `Cache-Control` de uma CDN?
Eles resolvem o mesmo problema geral em camadas diferentes - o cacheamento Cache-Control/CDN funciona para respostas HTTP públicas e não personalizadas, cacheadas fora do servidor de origem inteiramente, enquanto o cacheamento Redis geralmente lida com dados personalizados ou protegidos por autenticação que uma CDN pública não pode cachear com segurança.
O uso de Redis garante que um cache nunca esteja desatualizado?
Não - Redis resolve a consistência entre instâncias (um valor compartilhado por chave), não a desatualização em relação à origem. Um valor no Redis ainda pode estar desatualizado dentro de sua janela de TTL exatamente da mesma forma que um valor em cache em processo pode.