System Design/02 - Componentes/Databases2 min
Key-Value Store
- 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