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.
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
- Hot keys — detectar com
--hotkeys, mitigar com local cache ou sharding - Connection pool — nunca conexão por request; pool dimensionado
- Comandos bloqueantes — proibir KEYS, FLUSHALL sync, scripts longos
- Big keys — monitorar com
--bigkeys, particionar dados grandes - Locks — usar SET NX EX + Lua para release; considerar Redlock
- Escritas concorrentes — INCR/DECR atômico, WATCH/MULTI/EXEC
- 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_clientseblocked_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?