Referência técnica — Redis / Valkey

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).

TipoEncoding compactoEncoding promovido
stringint / embstr (≤44 bytes)raw (SDS heap)
hashlistpackhashtable
listlistpackquicklist
setintset / listpackhashtable
zsetlistpackskiplist + hashtable
Produção

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.

C
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.

Armadilha de produção

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.

No seu caso

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

BASH
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

PolicyComportamento
noevictionRecusa escritas com erro ao bater o limite (leituras continuam)
allkeys-lru / volatile-lruRemove a menos recentemente usada
allkeys-lfu / volatile-lfuRemove a menos frequentemente usada
volatile-ttlRemove a que está mais perto de expirar
Crítico pro seu ElastiCache

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:

BASH
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:

Cluster mode

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

BASH
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
Padrão correto

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

BASH
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

BASH
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

BASH
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

BASH
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

PYTHON
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.

RedisValkey
LicençaTri-license: AGPLv3 (desde Redis 8, mai/2025) ou SSPLv1/RSALv2BSD-3-Clause puro, sem copyleft
MantenedorRedis 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écnicoVector Sets nativo, Redis Query Engine mais maduroI/O multithreaded (ganhos de throughput em benchmarks da AWS)
Default AWSOpcionalElastiCache e MemoryDB usam como default em clusters novos
Timeline de licenciamento

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.

Não assuma paridade total

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ção
  • INFO memory — ver fragmentação, uso real vs. maxmemory
  • redis-cli --bigkeys — achar chaves grandes distorcendo o encoding sem necessidade
  • MEMORY USAGE key — custo de memória de uma chave específica