Performance e otimização
Otimizando operações no Redis
Técnicas avançadas de otimização — pipelining, batching, escolha de estruturas de dados, Lua scripts e redução de round-trips.
Nesta aula você vai
- Aplicar pipelining e batching para reduzir round-trips
- Escolher estruturas de dados otimais para cada padrão de acesso
- Usar Lua scripts e operações nativas para atomicidade sem overhead
Otimizando operações no Redis
Objetivos
Nesta aula você vai:
- Dominar pipelining e batching para eliminar latência de rede
- Escolher a estrutura de dados certa para cada padrão de acesso
- Aplicar Lua scripts e comandos nativos para operações atômicas eficientes
Conteúdo
A economia dos round-trips
Cada comando Redis viaja pelo protocolo RESP em um round-trip de rede: cliente envia, servidor processa, cliente recebe. Em localhost, um round-trip custa ~0,05 ms; entre zonas de disponibilidade, pode custar 1–5 ms. Multiplique por 100 comandos sequenciais e a latência explode — mesmo que cada operação no servidor leve microssegundos.
A otimização de operações Redis é, em essência, a arte de dizer mais com menos viagens.
Pipelining: o expresso de comandos
Pipelining envia múltiplos comandos sem esperar cada resposta individualmente:
# SEM pipeline: 3 round-trips (~3 ms em rede remota)
redis-cli SET chave:1 "valor1"
redis-cli SET chave:2 "valor2"
redis-cli SET chave:3 "valor3"
# COM pipeline: 1 round-trip
redis-cli --pipe <<EOF
SET chave:1 valor1
SET chave:2 valor2
SET chave:3 valor3
EOF
Em código (Node.js com ioredis):
const pipeline = redis.pipeline();
for (const item of items) {
pipeline.hset(`produto:${item.id}`, "nome", item.nome, "preco", item.preco);
}
const results = await pipeline.exec();
// 1000 HSET em 1 round-trip em vez de 1000
| Cenário | Sem pipeline | Com pipeline (100 cmds) |
|---|---|---|
| Round-trips | 100 | 1 |
| Latência total (1 ms RT) | ~100 ms | ~1 ms |
| Throughput | ~1.000 ops/s | ~100.000 ops/s |
Atenção: pipeline não é transação. Se um comando falha no meio, os anteriores já foram executados.
Batching com MGET e HMGET
Quando precisa de múltiplas chaves do mesmo tipo, use comandos batch nativos:
# Ruim: N round-trips
GET usuario:1
GET usuario:2
GET usuario:3
# Bom: 1 round-trip
MGET usuario:1 usuario:2 usuario:3
# Hash: buscar campo específico de múltiplas chaves
# (Redis 6.2+ não tem HMGET multi-key nativo — use pipeline)
redis-cli --pipe <<EOF
HGET carrinho:1 total
HGET carrinho:2 total
HGET carrinho:3 total
EOF
Escolha da estrutura de dados
A estrutura errada pode multiplicar memória e latência:
| Padrão de acesso | Estrutura ideal | Evitar |
|---|---|---|
| Objeto com campos nomeados | Hash (HSET/HGET) |
String JSON (serializar tudo) |
| Lista ordenada por score | Sorted Set (ZADD) |
List + sort na aplicação |
| Membros únicos (tags) | Set (SADD) |
List com verificação manual |
| Fila FIFO | List (LPUSH/RPOP) |
Sorted Set com timestamp |
| Contador simples | String (INCR) |
Hash com campo numérico |
| Bitmap de presença | Bitmap (SETBIT) |
Set com milhões de membros |
Exemplo: economia de memória com Hash
# Ineficiente: JSON serializado (~120 bytes com overhead)
SET usuario:42 '{"nome":"Ana","email":"a@b.com","idade":30}'
# Eficiente: Hash (~60 bytes com ziplist encoding)
HSET usuario:42 nome Ana email a@b.com idade 30
# Verificar encoding interno
redis-cli OBJECT ENCODING usuario:42
# "ziplist" ou "listpack" (Redis 7+)
Operações atômicas nativas
Evite GET + SET na aplicação (race condition). Use comandos atômicos:
# Rate limiting atômico
SET rate:ip:192.168.1.1 0 EX 60 NX # Cria com TTL se não existe
INCR rate:ip:192.168.1.1 # Incrementa atomicamente
GET rate:ip:192.168.1.1 # Verifica contagem
# GETSET: lê valor antigo e define novo (atômico)
GETSET contador:visitas 0
# SET com condição
SET lock:recurso:42 "owner-1" NX EX 30 # Lock distribuído
Lua scripts: lógica complexa no servidor
Para operações que envolvem múltiplos comandos com atomicidade:
# Script: transferir saldo entre contas (atômico)
redis-cli EVAL "
local origem = KEYS[1]
local destino = KEYS[2]
local valor = tonumber(ARGV[1])
local saldo = tonumber(redis.call('GET', origem) or '0')
if saldo < valor then
return 0
end
redis.call('DECRBY', origem, valor)
redis.call('INCRBY', destino, valor)
return 1
" 2 conta:42 conta:99 100
Vantagens do Lua no Redis:
- Atomicidade — script executa sem interrupção
- Menos round-trips — lógica no servidor
- Cache de script —
SCRIPT LOAD+EVALSHAevita reenviar código
# Carregar script uma vez
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Retorna SHA: a1b2c3d4...
# Executar por SHA (mais eficiente)
EVALSHA a1b2c3d4... 1 minha:chave
SCAN vs KEYS: iteração segura
# PERIGO em produção: bloqueia o servidor
KEYS usuario:*
# SEGURO: iteração incremental
SCAN 0 MATCH usuario:* COUNT 100
KEYS percorre todo o banco de uma vez — em datasets grandes, bloqueia o single-thread por segundos, degradando P99 de todos os clientes.
Otimizações de configuração
# Desabilitar comandos perigosos em produção
redis-cli CONFIG SET rename-command KEYS ""
redis-cli CONFIG SET rename-command FLUSHALL ""
# TCP keepalive para conexões longas
redis-cli CONFIG SET tcp-keepalive 300
# Connection pooling no cliente (não no servidor)
# Jedis: JedisPool com maxTotal=50
# ioredis: new Redis({ maxRetriesPerRequest: 3 })
Padrões de otimização por cenário
Cache-aside otimizado
# 1. Tentar cache (1 round-trip)
GET produto:1001
# 2. Miss → buscar DB → gravar cache (pipeline)
# SET produto:1001 <json> EX 300
Invalidação em lote
# Invalidar múltiplas chaves com pipeline
redis-cli --pipe <<EOF
DEL cache:produto:1001
DEL cache:produto:1002
DEL cache:produto:1003
EOF
Leaderboard eficiente
# Atualizar score (O(log N))
ZADD ranking:2024 1500 jogador:ana
# Top 10 (O(log N + M))
ZREVRANGE ranking:2024 0 9 WITHSCORES
# Rank de um jogador (O(log N))
ZREVRANK ranking:2024 jogador:ana
Medindo o impacto das otimizações
# Antes: sem pipeline
redis-benchmark -t set -n 10000 -c 1 -q
# Resultado: ~10.000 ops/s
# Depois: com pipeline de 16
redis-benchmark -t set -n 10000 -c 1 -P 16 -q
# Resultado: ~150.000 ops/s
# Latência real (sem pipeline)
redis-cli --latency-history -i 1
Resumo
- Pipelining e batching eliminam round-trips — maior ganho em redes com latência
- Escolha Hash sobre JSON serializado, Sorted Set para rankings, Bitmap para presença
- Lua scripts garantem atomicidade sem múltiplos round-trips
- Nunca use KEYS em produção — prefira SCAN incremental
Para refletir
Em um endpoint que busca 20 produtos por ID, quantos round-trips Redis sua implementação atual faz? Como reduziria para 1?