Performance e otimização

Evitando gargalos de concorrência

Identifique e resolva gargalos de concorrência no Redis — hot keys, connection pooling, locks distribuídos, contenção de single-thread e estratégias de sharding.

Avançado 35 min 30 pontos Leitura 0%

Nesta aula você vai

  • Identificar hot keys e aplicar estratégias de mitigação
  • Implementar connection pooling e gerenciar contenção de conexões
  • Usar locks distribuídos e padrões de concorrência seguros

Evitando gargalos de concorrência

Objetivos

Nesta aula você vai:

  • Identificar e mitigar hot keys que saturam partições do cluster
  • Configurar connection pooling adequado para alta concorrência
  • Implementar locks distribuídos e padrões de concorrência seguros

Conteúdo

O single-thread e seus inimigos

Redis processa comandos em um único thread principal. Essa decisão arquitetural — elogiada por eliminar locks internos — cria um paradoxo: o servidor é extremamente rápido em operações simples, mas vulnerável a contenção quando muitos clientes disputam as mesmas chaves ou quando comandos lentos bloqueiam a fila.

Como numa biblioteca com um único bibliotecário excepcionalmente eficiente: se cinquenta pessoas pedem o mesmo livro ao mesmo tempo, a fila explode — não importa quão rápido ele seja.

Hot keys: o problema da popularidade

Uma hot key é uma chave acessada desproporcionalmente mais que as demais. Em Redis Cluster, cada chave pertence a um hash slot em um nó específico — uma hot key concentra toda a carga em um único nó.

Redis Cluster (3 nós):

Nó 1: [slots 0-5460]     ← hot key "contador:global" vive aqui
Nó 2: [slots 5461-10922]   carga normal
Nó 3: [slots 10923-16383]  carga normal

Resultado: Nó 1 a 100% CPU, Nós 2 e 3 ociosos

Detectando hot keys

# Redis 4.0+: monitorar acessos por hot key
redis-cli --hotkeys

# Saída típica:
# [hotkey] contador:visitas: 45000 accesses
# [hotkey] config:global: 12000 accesses

# Alternativa: MONITOR (cuidado em produção — impacta performance)
redis-cli MONITOR | head -1000

# Via latência: chave específica lenta sob carga
redis-cli --latency -i 1

Estratégias de mitigação

1. Local cache (L1) na aplicação

// Cache local com TTL curto para hot keys
const localCache = new Map();
const LOCAL_TTL_MS = 1000;

async function getConfig() {
  const cached = localCache.get("config:global");
  if (cached && Date.now() - cached.ts < LOCAL_TTL_MS) {
    return cached.value;
  }
  const value = await redis.get("config:global");
  localCache.set("config:global", { value, ts: Date.now() });
  return value;
}

2. Sharding da hot key

# Em vez de uma chave:
INCR contador:visitas

# Distribuir em N shards:
INCR contador:visitas:shard:0
INCR contador:visitas:shard:1
INCR contador:visitas:shard:2
INCR contador:visitas:shard:3

# Leitura: somar shards
# shard = hash(client_id) % NUM_SHARDS

3. Read replicas para leitura

# Escrita no master
SET hotkey:dados "valor" EX 60

# Leitura nas replicas (eventual consistency)
# Cliente direciona GET para replica

Connection pooling: não abra conexão por requisição

Cada conexão TCP tem custo de setup (~1 ms). Em aplicações com milhares de requisições por segundo, abrir conexão por request é catastrófico.

Padrão Conexões Impacto
Conexão por request 10.000 req/s = 10.000 connects/s Esgota file descriptors
Pool de 50 conexões 50 conexões persistentes Estável
# Verificar conexões ativas
redis-cli INFO clients
# connected_clients:47
# blocked_clients:0

# Limite de conexões
redis-cli CONFIG SET maxclients 10000

# Conexões bloqueadas (BLPOP, BRPOP, WAIT)
# blocked_clients > 0 pode indicar contenção

Configuração de pool (exemplo Java com Lettuce):

// Pool configurado para 50 conexões max
GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig =
    new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(50);
poolConfig.setMaxIdle(20);
poolConfig.setMinIdle(5);

Comandos bloqueantes: os vilões silenciosos

Comandos que bloqueiam o servidor ou o cliente degradam P99:

Comando Bloqueia Alternativa
KEYS * Servidor inteiro SCAN incremental
FLUSHALL Servidor inteiro FLUSHALL ASYNC
BLPOP Cliente (espera) Consumer group com Stream
DEBUG SLEEP Servidor Nunca em produção
SORT em listas grandes Servidor Sorted Set nativo
Lua script longo Servidor (single-thread) Dividir em scripts menores
# Perigoso: bloqueia single-thread por segundos
redis-cli KEYS "usuario:*"

# Seguro: itera sem bloquear
redis-cli SCAN 0 MATCH "usuario:*" COUNT 200

Locks distribuídos

Para coordenar acesso exclusivo a recursos:

# Adquirir lock (atômico com NX + TTL)
SET lock:pedido:1001 "worker-42" NX EX 30
# OK = lock adquirido
# (nil) = lock já existe

# Liberar lock (apenas o dono)
# Script Lua para evitar liberar lock de outro processo:
redis-cli EVAL "
  if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
  else
    return 0
  end
" 1 lock:pedido:1001 worker-42

Problemas do lock Redis simples:

Problema Cenário Solução
Lock expira antes do trabalho terminar Processo lento Renovar TTL (Redlock)
Clock skew entre nós Active Replication Redlock com quorum
Falha após adquirir lock Processo morre TTL automático libera

Redlock (algoritmo de antirez):

# Adquirir lock em N instâncias independentes (quorum = N/2 + 1)
# Implementado em bibliotecas: Redisson (Java), redlock-py

Contenção em operações de escrita

# Ruim: race condition
# Processo A: GET estoque → 5
# Processo B: GET estoque → 5
# Processo A: SET estoque 4
# Processo B: SET estoque 4  ← deveria ser 3!

# Bom: operação atômica
DECR estoque:produto:1001
# Retorna novo valor atomicamente

# Bom: WATCH + MULTI/EXEC (optimistic locking)
redis-cli WATCH estoque:produto:1001
redis-cli GET estoque:produto:1001
# Se valor mudou entre WATCH e EXEC, transação falha
redis-cli MULTI
redis-cli SET estoque:produto:1001 3
redis-cli EXEC

Big keys e slow operations

# Identificar chaves grandes
redis-cli --bigkeys

# Identificar comandos lentos
redis-cli SLOWLOG GET 20
# Cada entrada: timestamp, duração (microsegundos), comando

# Configurar threshold do slowlog
redis-cli CONFIG SET slowlog-log-slower-than 10000  # 10ms
Tamanho da chave Risco Mitigação
String > 1 MB Latência de serialização Comprimir ou particionar
Hash > 10.000 campos HGETALL bloqueia HSCAN incremental
List > 1.000.000 itens LPUSH/RPOP lento Dividir em sub-listas
Set > 1.000.000 membros SISMEMBER lento Bitmap ou Bloom filter

I/O threads (Redis 6.0+)

Redis pode usar threads auxiliares para I/O de rede, liberando o thread principal:

# Configurar I/O threads
redis-cli CONFIG SET io-threads 4
redis-cli CONFIG SET io-threads-do-reads yes

# Verificar
redis-cli INFO server | grep io_threads

Útil quando o gargalo é rede (muitas conexões), não CPU de comandos.

Checklist anti-gargalo

  1. Hot keys — detectar com --hotkeys, mitigar com local cache ou sharding
  2. Connection pool — nunca conexão por request; pool dimensionado
  3. Comandos bloqueantes — proibir KEYS, FLUSHALL sync, scripts longos
  4. Big keys — monitorar com --bigkeys, particionar dados grandes
  5. Locks — usar SET NX EX + Lua para release; considerar Redlock
  6. Escritas concorrentes — INCR/DECR atômico, WATCH/MULTI/EXEC
  7. Slowlog — monitorar comandos > 10 ms continuamente

Resumo

  • Hot keys concentram carga em um nó — mitigue com local cache, sharding ou replicas
  • Connection pooling é obrigatório; monitore connected_clients e blocked_clients
  • Locks distribuídos exigem TTL + verificação de dono na liberação
  • Proíba KEYS e comandos bloqueantes em produção; use SCAN e operações atômicas

Para refletir

"Concorrência não é sobre velocidade — é sobre justiça na fila. O Redis é rápido, mas injusto se todos querem o mesmo livro."

Qual hot key existe no seu sistema? Como você a detectaria e mitigaria?