trilha

System Design/02 - Componentes/Databases2 min

Key-Value Store

Perguntas-guia
  • Que problema isso resolve?
  • Quando usar / quando NÃO usar?
  • Qual o principal trade-off?
  • Como isso falha em produção?

Conceito

O modelo mais simples: uma chave, um valor opaco. Acesso por chave em O(1); nenhuma consulta por conteúdo.

sessao:abc123  → { userId: 42, expira: ... }
carrinho:u42   → [ ... ]

Exemplos: Redis, Memcached, DynamoDB, etcd, Riak.

Onde brilha

Uso Por quê
Cache Latência sub-milissegundo
Sessão Acesso por token, TTL nativo
Rate limiting Contador atômico
Fila / lock Estruturas do Redis
Feature flags Leitura constante, escrita rara
Perfil, carrinho Acesso sempre por ID

Redis vs Memcached

Redis Memcached
Estruturas Lista, set, sorted set, hash, stream, pub/sub Só chave-valor
Persistência RDB + AOF Nenhuma
Replicação / failover Sim (Sentinel, Cluster) Não
Multi-thread Não (I/O sim) Sim

Trade-offs

A simplicidade é o ganho e o limite. Não há consulta por valor, não há junção, não há índice secundário — se você precisa "todos os usuários de São Paulo", o modelo não responde. A saída é manter você mesmo um índice invertido (cidade:SP → [ids]), com toda a manutenção que isso implica.

Isso força a modelar pelas consultas: cada padrão de acesso vira uma chave. Duplicação é esperada, e um dado alterado precisa ser atualizado em todas as chaves que o contêm.

Dois problemas operacionais recorrentes:

Hot key — uma chave muito acessada concentra tráfego num único nó, e sharding não ajuda. Saídas: replicar a chave em sufixos ou cachear localmente.

Big key — um valor de dezenas de MB (uma lista que cresceu sem limite) bloqueia o Redis, que é single-threaded, durante a serialização. Uma operação KEYS * ou DEL numa chave gigante em produção é um incidente clássico.

E, quando usado como banco primário e não como cache: sem persistência configurada, um restart perde tudo. Redis com AOF é durável; Redis padrão em memória não é.

Exemplo prático

Rate limiting por usuário, em uma operação atômica:

INCR  rate:u42:minuto-1
EXPIRE rate:u42:minuto-1 60
→ se > 100, rejeita com 429

Duas operações, latência sub-milissegundo, e o TTL faz a limpeza sozinho — nenhum job de expurgo é necessário.

Fazer o mesmo num banco relacional exigiria uma tabela, um índice, uma transação para o incremento atômico e um job periódico para apagar registros vencidos. É a diferença entre o modelo servir ao problema e o problema ser forçado no modelo.

Relacionado


Parte de Databases · roadmap.sh/system-design

Buscar

Busca por título, seção e texto das notas