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.

Avançado 30 min 25 pontos Leitura 0%

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 scriptSCRIPT LOAD + EVALSHA evita 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?