Armazenamento de Dados e Operações CRUD
Como o keyspace é estruturado internamente, o que acontece em memória por trás de cada tipo de dado, e os comandos práticos de CRUD — em Redis, com Valkey como alvo de produção.
1. Modelo mental
Redis (e Valkey, seu fork compatível) não é um banco relacional. É um key-value store in-memory com estruturas de dados ricas por trás de cada chave. Não existe schema, não existe SELECT no sentido SQL, e a "query" que você conhece de Postgres/MySQL não existe aqui — você troca expressividade de consulta por latência de sub-milissegundo.
Isso muda como você pensa em modelagem: em vez de normalizar em tabelas, você desenha o access pattern primeiro (que chave, que tipo, que TTL) e a estrutura de dados segue o padrão de leitura/escrita que você já vai fazer — o mesmo raciocínio de chave que você já aplica no DynamoDB.
2. Como o dado é armazenado internamente
2.1 O keyspace é um dicionário
Cada database é, internamente, uma hash table global (dict) mapeando key → redisObject. Array de buckets, resolução de colisão por chaining, e rehashing incremental quando o load factor cresce — para não travar o event loop com um rehash gigante de uma vez só.
2.2 redisObject: tipo declarado vs. encoding físico
Toda chave aponta para um redisObject com dois atributos: type (o que você vê em TYPE key — string/hash/list/set/zset/stream) e encoding (como está fisicamente representado — visível via OBJECT ENCODING key).
| Tipo | Encoding compacto | Encoding promovido |
|---|---|---|
| string | int / embstr (≤44 bytes) | raw (SDS heap) |
| hash | listpack | hashtable |
| list | listpack | quicklist |
| set | intset / listpack | hashtable |
| zset | listpack | skiplist + hashtable |
Os thresholds de promoção (hash-max-listpack-entries, hash-max-listpack-value, set-max-intset-entries) controlam quando uma estrutura compacta vira uma estrutura O(1)/O(log n) com overhead de ponteiros por elemento. Num cluster com milhões de hashes pequenos, manter tudo em listpack pode ser a diferença entre caber em cache.r7g.large ou precisar de duas classes de instância a mais.
2.3 SDS — a string interna do Redis
Strings não são C-strings terminadas em \0. São SDS (Simple Dynamic Strings): strlen em O(1), binary-safety (guarda bytes \0 no meio), crescimento amortizado.
struct sdshdr {
uint64_t len; // tamanho atual
uint64_t alloc; // capacidade alocada
unsigned char flags;
char buf[]; // dado real
};
3. Persistência — o que sobrevive a um restart
RDB (snapshot): fork() + copy-on-write, escreve um binário compacto do dataset num ponto no tempo. Rápido de restaurar, mas perde tudo desde o último snapshot.
O fork() do RDB pode causar pico de latência e, em datasets grandes com alta escrita, pico temporário de memória (copy-on-write duplicando páginas sujas). Em instâncias com pouca margem livre, isso já derrubou processo por OOM no meio de um snapshot.
AOF (append-only file): log de cada comando de escrita, replay no restart. Configurável via appendfsync:
always— fsync a cada escrita (mais seguro, mais lento)everysec— fsync 1x/segundo (default — até 1s de perda em crash)no— deixa o SO decidir (mais rápido, risco maior)
Desde Redis 4+/Valkey, o padrão é RDB + AOF híbrido: o AOF usa preamble RDB pra reduzir o tamanho do rewrite.
Persistência não é backup — é durabilidade de curto prazo pro próprio processo. Seu padrão de backup 90 dias pra DynamoDB/S3 Parquet continua sendo a fonte de verdade de longo prazo. RDB/AOF resolve "o processo caiu e voltou", não "preciso reconstruir o estado de 3 meses atrás".
4. TTL e eviction
EXPIRE key 3600 # expira em 3600s
PEXPIRE key 3600000 # mesma coisa em ms
TTL key # segundos restantes (-1 = sem TTL, -2 = não existe)
PERSIST key # remove o TTL
Expiração acontece de duas formas simultâneas: passiva (deleta ao tentar ler chave expirada) e ativa (ciclo de background varre amostras do keyspace). Em replicação, o replica não expira por conta própria — espera o DEL explícito do master, evitando inconsistência por clock skew.
Eviction — quando a memória enche
| Policy | Comportamento |
|---|---|
noeviction | Recusa escritas com erro ao bater o limite (leituras continuam) |
allkeys-lru / volatile-lru | Remove a menos recentemente usada |
allkeys-lfu / volatile-lfu | Remove a menos frequentemente usada |
volatile-ttl | Remove a que está mais perto de expirar |
Se você usa Redis/Valkey como fonte de estado (checkpoints LangGraph, locks distribuídos com SET NX) e não como cache descartável, noeviction é praticamente obrigatório — qualquer eviction policy ali pode apagar um checkpoint ou lock ativo silenciosamente sob pressão de memória. Cache de resposta de LLM, por outro lado, é candidato natural a allkeys-lru.
5. Sobre o "SELECT" — provável fonte de confusão
Em Redis/Valkey, SELECT não é uma query. É a troca de database lógico dentro da mesma instância:
SELECT 1 # muda pro database índice 1 (default: 0-15 databases)
É um namespace bruto por índice numérico, sem nome, sem isolamento de permissão. Duas pegadinhas de produção:
Não suporta múltiplos databases — só existe o DB 0. Se seu cluster ElastiCache/Valkey está em modo cluster (o que você provavelmente quer pra sharding automático), SELECT nem está disponível.
Não confunda com "consulta filtrada" — não existe WHERE. Algo tipo query é resolvido por desenho de chave (SCAN MATCH pattern*, O(n), não rodar em produção sob carga) ou por Sorted Set (ZRANGEBYSCORE) quando o acesso é por range.
6. CRUD — Strings
SET user:123:name "Leo"
SET session:abc "token_data" EX 3600 # com TTL, expira em 1h
SET lock:job:42 "1" NX EX 30 # só seta se não existir (lock atômico)
GET user:123:name
MGET user:123:name user:123:email # múltiplas chaves, 1 round-trip
DEL user:123:name
EXISTS user:123:name # retorna 0 ou 1
INCR counter:requests # atômico, útil pra contadores/rate limit
SET key value NX EX ttl é o padrão certo pra lock distribuído — atômico em uma única operação. O anti-padrão antigo (SETNX + EXPIRE como dois comandos) tem janela de race condition: um crash entre os dois deixa a chave sem TTL, travada pra sempre. Pelo que você já usa SET NX + Lua nos seus nodes idempotentes, você já está no caminho certo — o Lua entra quando o check-then-act precisa de mais de uma leitura/escrita atômica que um único comando não cobre.
7. CRUD — Estruturas ricas
Isso substitui "SELECT" na prática — cada tipo tem sua própria forma de consulta.
Hash — objeto com campos
HSET user:123 name "Leo" role "engineer" active "1"
HGET user:123 name
HGETALL user:123 # todos os campos — cuidado: O(n) no tamanho do hash
HMGET user:123 name role # só os campos que você precisa
HDEL user:123 active
HINCRBY user:123 login_count 1
Use hash em vez de serializar um JSON inteiro numa string quando você precisa atualizar ou ler campos individuais sem reescrever o objeto inteiro — evita race condition de read-modify-write no cliente.
List — fila/pilha, ordem de inserção
LPUSH queue:tasks "task1" # insere na cabeça
RPUSH queue:tasks "task2" # insere na cauda
LRANGE queue:tasks 0 -1 # lista tudo
LPOP queue:tasks # remove e retorna da cabeça
BRPOP queue:tasks 5 # pop bloqueante, timeout 5s
Set — associação sem ordem, sem duplicata
SADD tags:article:1 "ai" "aws" "python"
SMEMBERS tags:article:1
SISMEMBER tags:article:1 "ai" # O(1), checagem de pertencimento
SREM tags:article:1 "python"
Sorted Set (ZSET) — a estrutura mais subestimada
ZADD leaderboard 100 "player1" 200 "player2"
ZRANGE leaderboard 0 -1 WITHSCORES # ordenado por score
ZRANGEBYSCORE leaderboard 150 300 # range query real
ZSCORE leaderboard "player1"
ZINCRBY leaderboard 10 "player1"
Score pode ser um timestamp — ZSET vira índice ordenado por tempo (ex.: mensagens de um thread_id, eventos de um trace). "Me dê os últimos N eventos entre timestamp X e Y" é ZRANGEBYSCORE, não SCAN.
8. Python (redis-py) na prática
import redis
# Connection pool — crítico em ECS/Fargate: NÃO crie uma conexão nova por invocação,
# reusar pool evita esgotar conexões do ElastiCache sob concorrência de múltiplas tasks
pool = redis.ConnectionPool(
host="seu-cluster.cache.amazonaws.com",
port=6379,
max_connections=50,
decode_responses=True,
)
r = redis.Redis(connection_pool=pool)
# CRUD básico
r.set("session:abc", "token_data", ex=3600)
val = r.get("session:abc")
r.delete("session:abc")
r.exists("session:abc")
# Hash
r.hset("user:123", mapping={"name": "Leo", "role": "engineer"})
user = r.hgetall("user:123")
# Pipeline — agrupa comandos em 1 round-trip (não é atômico por padrão)
pipe = r.pipeline(transaction=False)
pipe.set("a", "1")
pipe.set("b", "2")
pipe.execute()
# Transação real (MULTI/EXEC) — atômica, sem leitura condicional no meio
with r.pipeline(transaction=True) as pipe:
pipe.multi()
pipe.incr("counter")
pipe.expire("counter", 60)
pipe.execute()
# Lua script — check-and-act atômico (padrão que você já usa nos nodes idempotentes)
release_lock = r.register_script("""
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
""")
release_lock(keys=["lock:job:42"], args=["owner_id_123"])
9. Redis vs. Valkey — o que realmente mudou
O fork não foi cosmético — foi consequência direta de licenciamento, e as duas linhas divergiram desde então.
| Redis | Valkey | |
|---|---|---|
| Licença | Tri-license: AGPLv3 (desde Redis 8, mai/2025) ou SSPLv1/RSALv2 | BSD-3-Clause puro, sem copyleft |
| Mantenedor | Redis Ltd. (empresa) | Linux Foundation (AWS, Google, Oracle, Ericsson) |
| Compatibilidade | ~90% de paridade de comandos em 2026 — não mais 100% intercambiável às cegas | |
| Diferencial técnico | Vector Sets nativo, Redis Query Engine mais maduro | I/O multithreaded (ganhos de throughput em benchmarks da AWS) |
| Default AWS | Opcional | ElastiCache e MemoryDB usam como default em clusters novos |
Até Redis 7.2.4 era BSD. De Redis 8.0 (maio/2025) em diante, Redis passou a oferecer AGPLv3 como opção open-source ao lado das licenças source-available. O fork Valkey nasceu quando grandes provedores de nuvem responderam à mudança original forkando o Redis 7.2 sob a Linux Foundation.
Por volta de 2026, os dois projetos já divergiram de forma real — ~90% compatíveis no nível de comandos, mas o suficiente pra escolha não ser mais só inércia. Ao migrar código Redis pra Valkey, teste — principalmente em módulos como vetorial/search, onde a divergência é maior.
Pra sua decisão (Valkey em produção, Redis como referência de conceitos/código): é a escolha pragmática correta — os comandos do dia a dia (GET/SET/HSET/ZADD/Lua) são idênticos, redis-py funciona sem alteração contra Valkey, e você ganha licença BSD sem risco de copyleft de rede.
10. Aplicando ao seu contexto
SET key value NX→ o primitivo por trás do seu padrão de lock distribuído pra nodes idempotentes- Lua scripts → entram quando o lock/estado precisa de check-and-act atômico que um único comando não resolve sozinho (ex.: "libera o lock só se eu ainda sou o dono")
PK=USER#{user_id}→ forma natural de simular "tabelas" num keyspace plano — particionamento lógico, já que não existe schema nem SELECT- TTL em memória de curto prazo →
EXPIREé o mecanismo natural pra sessões/checkpoints de vida curta, complementando o backup de 90 dias no DynamoDB/S3 como long-term store
Próximos passos
OBJECT ENCODING key— inspecionar o encoding real de qualquer chave em produçãoINFO memory— ver fragmentação, uso real vs.maxmemoryredis-cli --bigkeys— achar chaves grandes distorcendo o encoding sem necessidadeMEMORY USAGE key— custo de memória de uma chave específica