03 - In-Memory (Redis, Memcached)
video: https://youtu.be/... no frontmatter da nota.In-Memory (Redis, Memcached)
Bancos in-memory mantêm o conjunto de trabalho primariamente na memória RAM, trocando durabilidade robusta por latência de sub-milissegundo e throughput elevado. Eles ocupam posições críticas na arquitetura de produção: camada de cache na frente do banco principal, armazenamento de sessão, coordenador de estado efêmero, fila leve e bloqueio distribuído. Redis é o mais completo da categoria, com dezenas de estruturas de dados nativas, replicação, cluster e persistência opcional; Memcached é o especialista simples, um cache de chave-valor multithread sem complicações. Em um serviço típico, uma leitura que custaria dezenas de milissegundos no banco de dados cai para frações de milissegundo no in-memory, e essa diferença determina se o SLO de p99 é cumprido ou violado. A escolha do motor e do modo de operação, porém, precisa considerar um fator novo: o mercado mudou de rumo entre 2024 e 2025, com mudança de licença do Redis e consolidação do Valkey como alternativa open source sob a Linux Foundation.
Conceito
Um banco in-memory mantém os dados em estruturas residentes em RAM, acessadas por processos no mesmo host ou pela rede local, com custo de leitura tipicamente entre dezenas de microssegundos e um milissegundo. A memória é volátil, então a durabilidade depende de estratégias explícitas: snapshots periódicos, log appendido, replicação assíncrona para um standby, ou nenhuma delas quando os dados são recriáveis. Redis organiza tudo como estruturas de dados nomeadas, strings, hashes, listas, sets, sorted sets e streams, cada uma com operações atômicas próprias, o que move parte da lógica da aplicação para dentro do servidor. Memcached expõe apenas um mapa de chave para valor, com política de eviction LRU por slabs, e resolve bem exatamente um caso: cache distribuído sem estado. Ambos são single-purpose por natureza; usá-los como fonte primária de dados importantes exige consciência dos riscos de perda.
Que problema resolve
O problema central que o in-memory ataca é a latência e o custo do caminho quente de leitura. Bases de dados em disco sofrem com padrões de acesso aleatório, consultas agregadas repetidas e trabalho de CPU que se repete para o mesmo resultado. O in-memory resolve quatro dores recorrentes em produção:
- Latência de cache: resultados de consultas caras ficam prontos em memória, protegendo o banco de dados de trafego repetido e do thundering herd.
- Sessão e estado efêmero: sessões de usuário, tokens e carrinhos de curta vida residem em memória com TTL automático, sem sujar o banco relacional.
- Coordenação leve: contadores, locks distribuídos com TTL, rate limiting e idempotência por chave usam operações atômicas de servidor.
- Fila e streaming simples: listas e streams do Redis cobrem buffers de trabalho e eventos de curto prazo sem operar um broker completo.
A regra de saudabilidade é simples: tudo o que vive só no in-memory deve ser recriável a partir de fontes duráveis, seja por reprocessamento, seja por expiração natural.
Funcionamento interno
O Redis opera um laço de eventos single-thread para comandos de dados, o que garante atomicidade por comando e evita locks, mas faz o uso de CPU ser limitado a um núcleo para a execução principal; desde a versão 6, I/O de rede pode ser multithread, e a versão 8 ampliou o multithreading para comandos de leitura e gravação. Toda operação de escrita vai para o replication buffer e, conforme a política, para a persistência. Memcached, ao contrário, é multithread desde o início, escalando CPU com o número de threads, e não tem persistência nem replicação; sua alta disponibilidade vem do padrão de cache farol, em que a perda de um nó apenas redistribui chaves via hash consistente. A política de eviction (volatile-lru, allkeys-lru, noeviction) define o que acontece quando a memória esgota, e essa escolha é uma decisão de arquitetura, não um detalhe de configuração.
# Estruturas de dados do Redis em operação (redis-cli)
SET sessao:user:42 "eyJhbGciOi..." EX 3600 # string com TTL de 1 hora
HSET produto:4821 nome "Fone Pro" preco 349.90 # hash: campos nomeados
LPUSH fila:emails "job:1001" # lista: fila FIFO de trabalho
BRPOP fila:emails 5 # consumidor bloqueante
ZADD ranking:vendas 1203 "loja:17" # sorted set com score
ZREVRANGE ranking:vendas 0 9 WITHSCORES # top 10 por score
# Stream para eventos com grupos de consumidor
XADD eventos:pedidos '*' id 1001 status pago
XGROUP CREATE eventos:pedidos processadores 0
XREADGROUP GROUP processadores worker-1 COUNT 10 BLOCK 2000 STREAMS eventos:pedidos '>'
# redis.conf: persistência e limites em produção
appendonly yes # AOF: log de cada escrita (durabilidade fina)
appendfsync everysec # compromisso: perde no máximo 1 segundo
save 900 1 # RDB: snapshot a cada 15 min se 1 mudança
maxmemory 6gb # teto de memória antes da eviction
maxmemory-policy allkeys-lru # evicta as menos usadas sob pressão
Persistência RDB e AOF
O Redis oferece duas formas complementares de persistência, e a combinação delas define o compromisso entre desempenho e risco de perda. RDB (Redis Database) grava snapshots binários do dataset em pontos discretos, via fork do processo; é compacto, rápido de restaurar e ideal para backup e recuperação de desastre, mas perde tudo o que aconteceu desde o último snapshot. AOF (Append Only File) registra cada comando de escrita, com opções de sincronização que vão de always (sem perda, custo alto) a everysec (ponto de equilíbrio usual). Em produção séria, a prática comum é habilitar os dois: AOF para janela mínima de perda, RDB para restaurações rápidas e backups externos. Replicação assíncrona para um réplica em outra zona adiciona sobrevivência de nó, mas lembre que replicação assíncrona admite perda de escritas recentes no failover; chaves críticas com exigência de absoluto, como saldo, não pertencem aqui.
Aplicação prática: padrões de cache
O valor do in-memory aparece quando o padrão de cache é escolhido conscientemente para cada fluxo. O padrão cache-aside é o mais comum: a aplicação consulta o cache, e na falta de hit busca no banco e popula o cache com TTL; simples, tolerante a queda do cache, mas sujeito a dados obsoletos até a expiração. O write-through escreve no cache e no banco na mesma operação, mantendo o cache sempre coerente ao custo de latência na escrita; o write-behind confirma a escrita no cache e persiste em lote depois, rápido e arriscado em caso de queda antes do flush. Para proteger contra stampede, combine lock curto por chave com recálculo assíncrono, e sirva stale-while-revalidate durante a reconstrução. Cache de agregados (respostas de API já montadas), de sessão e de rate limiting são os três usos que mais retornam investimento imediato em serviços web.
# Cache-aside com proteção contra stampede (pseudo-pseudocódigo)
def obter_produto(sku):
chave = f"v2:produto:{sku}" # prefixo de versão para invalidação
dados = redis.get(chave)
if dados:
return json.loads(dados)
if not redis.set(f"lock:{chave}", worker_id, nx=True, ex=5):
return servir_stale_ou_esperar(chave) # evita stampede
dados = banco.buscar_produto(sku) # fonte da verdade
redis.set(chave, json.dumps(dados), ex=600 + jitter(60))
redis.delete(f"lock:{chave}")
return dados
Redis, Valkey e o cenário de licenciamento
O panorama de licenciamento mudou de forma relevante para decisões de produção. Em 2024, a Redis Ltd. abandonou o modelo BSD 3-Clause, adotando licenças de código-fonte disponível (RSALv2 e SSPLv1), o que motivou o fork Valkey, sob BSD 3-Clause e governado pela Linux Foundation, mantido por engenheiros de grandes provedores de nuvem e usuários corporativos. Com o Redis 8, a empresa voltou a oferecer uma opção open source sob AGPLv3, além das licenças anteriores, enquanto o Valkey seguiu evolução independente; o Valkey 9 conta com módulos oficiais de JSON, Bloom e busca vetorial. Para a arquitetura, a implicação prática é de risco de fornecedor e continuidade: o Valkey é o caminho com governança comunitária e licença permissiva, e é o default em várias ofertas gerenciadas de nuvem; o Redis 8 com AGPLv3 reapresenta um caminho open source corporativo, com recursos avançados embutidos e restrições típicas da AGPL em serviços de rede. O protocolo RESP permanece compatível, o que mantém clientes e bibliotecas intercambiáveis e reduz o custo de migração; decida pelo modelo de governança que seu jurídico e seu time de operações suportam no horizonte de cinco anos.
| Aspecto | Redis | Valkey | Memcached |
|---|---|---|---|
| Estruturas de dados | Ricas: hashes, sets, sorted sets, streams | Ricas (fork do Redis 7) | Apenas chave-valor |
| Persistência | RDB e AOF opcionais | RDB e AOF opcionais | Nenhuma |
| Replicação e cluster | Sim (replica sets e cluster) | Sim (compatível) | Não; hash consistente no cliente |
| Licença atual | AGPLv3 opcional (v8), RSALv2, SSPLv1 | BSD 3-Clause | BSD 3-Clause |
| Governança | Redis Ltd. | Linux Foundation | Comunidade |
| Multithread | I/O e, na v8, comandos (opcional) | I/O (opcional) | Sim, nativo |
| Ponto forte | Ecossistema e recursos avançados | Governança aberta e compatibilidade | Simplicidade extrema como cache |
Alta disponibilidade e escala
As duas arquiteturas de escala do mundo in-memory são a replicação master-replica e o cluster particionado. Em ambientes pequenos e médios, um par master-replica com sentinela (ou o equivalente gerenciado com failover automático) cobre a necessidade e mantém o modelo mental simples. O cluster particionado divide o espaço de chaves em slots distribuídos entre nós, escala horizonte de memória e de throughput, mas cobra regras de multi-key, migração de slots durante resharding e clientes capazes de descobrir a topologia. Memcached escala por hash consistente no cliente: adicionar ou remover nós redistribui apenas parte das chaves, e a perda de um nó corresponde a um degrau temporário na taxa de acerto. Para trabalho pesado de leitura, réplicas de leitura absorvem volume; para escrita massiva, avalie sharding pela chave de aplicação e mantenha acessos multi-chave dentro do mesmo shard com hashes tags.
| Aspecto | Master-replica | Cluster particionado | Memcached distribuído |
|---|---|---|---|
| Complexidade operacional | Baixa | Alta | Baixa |
| Escala de memória | Nó único | Horizontal | Horizontal |
| Multi-key | Livre | Mesmo slot apenas | Livre (lado cliente) |
| Failover | Sentinela ou gerenciado | Automático entre nós | Nenhum; miss temporário |
| Caso típico | Sessão, cache pequeno e médio | Dataset grande por nó | Cache web puro |
Erros comuns
- Usar chaves sem TTL em cache, acumulando memória cheia e eviction agressiva que derruba a taxa de acerto de todo o sistema.
- Chaves grandes (big keys), como sets com milhões de membros, que bloqueiam o laço do Redis em deleções e em leituras inteiras; substitua por chunking ou particionamento da chave.
KEYS *em produção, comando O(n) que congela o servidor; useSCANcom cursor em batches.- Tratar o cache como fonte da verdade sem estratégia de invalidação, servindo dados obsoletos por horas após uma atualização.
- Cluster sem consciência de multi-key: comandos que acessam chaves de slots diferentes falham, e hashes tags (
{user:42}) precisam ser planejados. - Ignorar a política de eviction configurada, surpreendendo-se quando o nó passa a rejeitar escritas (noeviction) ou a descartar dados usados.
- Transações financeiras de saldo em in-memory sem durabilidade adequada, aceitando silenciosamente perda de escritas no failover.
- Memória e conexões sem limites no cliente, onde um pool mal configurado esgota o maxclients do servidor e derruba o serviço inteiro.
- TTL informado em segundos onde a biblioteca espera milissegundos (ou vice-versa), gerando chaves que expiram em instantes quando deveriam durar horas.
- Fila de trabalho implementada em lista sem ack nem reentrega, perdendo jobs quando o consumidor morre no meio do processamento.
Boas práticas
- Defina TTL em todas as chaves de cache e use jitter na expiração para evitar sincronização de miss em massa.
- Adote o padrão cache-aside como padrão de leitura e versionamento na chave (prefixo de versão) para invalidação em cascata barata.
- Monitore hit ratio, latência por comando, memória usada versus maxmemory, número de conexões e chaves grandes com análise periódica.
- Use pipeline para lotes de comandos e evite round trips serializados em laços de aplicação.
- Para locks distribuídos, exija TTL no lock e token único do dono, tratando o lock como sugestão de coordenação, não como garantia absoluta.
- Dimensione memória com folga de 30 a 40 por cento e teste o comportamento de eviction com carga real antes do go-live.
- Escolha motor e licença com horizonte de governança: Valkey quando governança aberta BSD for requisito, Redis quando os recursos da v8 e a AGPLv3 forem aceitáveis.
- Separe instâncias por classe de dado e criticidade: sessão, cache e filas leves em nós distintos, para que uma explosão de uma classe não evicte a outra.
- Feche o ciclo de vida: alerte antes do teto de memória, registre chaves sem TTL em auditoria semanal e teste restauração RDB/AOF periodicamente.
- Publique métricas do in-memory no mesmo observabilidade do serviço (acerto, evictions, latência p99), correlacionando degradação de aplicação com comportamento do cache.
- Documente o contrato de perda aceitável por classe de dado: o que pode evaporar sem prejuízo e o que, se sumir, precisa de reconstrução automática.