Pipelining do Redis

Pipelining permite enviar comandos em lote, reduzindo viagens de ida e volta na rede e melhorando drasticamente o desempenho.

O que é Pipelining?

Abordagem Tradicional

Cada comando requer uma viagem de ida e volta na rede:

Cliente                   Servidor Redis
  |                         |
  |----- SET key1 -------->|
  |<----- OK --------------|
  |                         |
  |----- SET key2 -------->|
  |<----- OK --------------|
  |                         |
  |----- SET key3 -------->|
  |<----- OK --------------|

3 comandos = 6 transferências de rede (3 requisições + 3 respostas)

Abordagem com Pipelining

Múltiplos comandos enviados de uma vez:

Cliente                   Servidor Redis
  |                         |
  |----- SET key1 -------->|
  |----- SET key2 -------->|
  |----- SET key3 -------->|
  |                         |
  |<----- OK --------------|
  |<----- OK --------------|
  |<----- OK --------------|

3 comandos = 2 transferências de rede (1 requisição em lote + 1 resposta em lote)

💡 Ganho de desempenho: Pipelining reduz viagens de ida e volta na rede, melhorando drasticamente o desempenho — especialmente quando a latência da rede é alta.

Como o Pipelining Funciona

Efeito da Latência de Rede

Assumindo uma viagem de ida e volta de 1ms na rede:

Abordagem tradicional:
- 100 comandos = 100 viagens = 100ms de latência de rede
- Mais tempo de processamento Redis (assumir 0.1ms/comando) = 10ms
- Tempo total = 100ms + 10ms = 110ms

Pipelining:
- 100 comandos = 1 viagem = 1ms de latência de rede
- Mais tempo de processamento Redis = 10ms
- Tempo total = 1ms + 10ms = 11ms

Melhoria de desempenho: 110ms → 11ms, um aumento de 10x!

Características do Pipeline

⚠️ Observação: Pipelining não é uma transação e não garante atomicidade. Use MULTI/EXEC para atomicidade.

Usando Pipelining

Modo Pipeline do redis-cli

BASH
# Usar modo pipeline do redis-cli
echo -e "SET key1 value1\nSET key2 value2\nSET key3 value3" | redis-cli

# Ou ler de um arquivo
cat commands.txt | redis-cli

Pipelining em Python

PYTHON
import redis

r = redis.Redis(host='localhost', port=6379)

# Criar pipeline
pipe = r.pipeline()

# Adicionar comandos ao pipeline
pipe.set('key1', 'value1')
pipe.set('key2', 'value2')
pipe.set('key3', 'value3')

# Executar pipeline
resultados = pipe.execute()
print(resultados)  # [True, True, True]

Pipelining em Java (Jedis)

JAVA
Jedis jedis = new Jedis("localhost", 6379);

Pipeline pipeline = jedis.pipelined();

pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.set("key3", "value3");

List<Object> results = pipeline.syncAndReturnAll();

Pipelining em Node.js (ioredis)

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

const pipeline = redis.pipeline();

pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.set('key3', 'value3');

const results = await pipeline.exec();

Pipeline vs Transações

Pipeline + Transações

Pipelining pode ser combinado com transações:

PYTHON
import redis

r = redis.Redis()

# Criar pipeline com transação habilitada
pipe = r.pipeline(transaction=True)

pipe.set('key1', 'value1')
pipe.set('key2', 'value2')
pipe.incr('counter')

# Executar transação
resultados = pipe.execute()

Pipeline vs Transação

Aspecto Pipeline Transação
Atomicidade ❌ Não ✅ Sim
Isolamento ❌ Não ✅ Sim
Ganho de desempenho ✅ Significativo ✅ Significativo
Envio em lote ✅ Sim ✅ Sim
Caso de uso Operações em lote, desempenho Operações em lote que precisam de atomicidade
💡 Escolha: Use pipeline quando a atomicidade não for necessária. Use transações ou pipeline+transação quando a atomicidade for necessária.

Casos de Uso do Pipeline

Caso de Uso 1: Configuração em Lote

PYTHON
import redis

r = redis.Redis()
pipe = r.pipeline()

# Definir 1000 chaves em lote
for i in range(1000):
    pipe.set(f'key:{i}', f'value:{i}')

resultados = pipe.execute()
print(f'Definidas {len(resultados)} chaves')

Caso de Uso 2: Obtenção em Lote

PYTHON
import redis

r = redis.Redis()
pipe = r.pipeline()

# Obter 1000 chaves em lote
chaves = [f'key:{i}' for i in range(1000)]
for chave in chaves:
    pipe.get(chave)

valores = pipe.execute()
print(f'Obtidos {len(valores)} valores')

Caso de Uso 3: Exclusão em Lote

PYTHON
import redis

r = redis.Redis()
pipe = r.pipeline()

# Exclusão em lote
chaves = ['key1', 'key2', 'key3', 'key4', 'key5']
for chave in chaves:
    pipe.delete(chave)

resultados = pipe.execute()
print(f'Deletadas {sum(resultados)} chaves')

Caso de Uso 4: Importação de Dados

PYTHON
import redis
import json

r = redis.Redis()
pipe = r.pipeline()

# Importar dados de arquivo JSON
with open('data.json', 'r') as f:
    dados = json.load(f)
    
    for item in dados:
        chave = f"user:{item['id']}"
        valor = json.dumps(item)
        pipe.set(chave, valor)

pipe.execute()
print('Importação concluída')

Caso de Uso 5: Atualização em Lote de Contadores

PYTHON
import redis

r = redis.Redis()
pipe = r.pipeline()

# Atualizar contadores em lote
contadores = {
    'article:1:views': 10,
    'article:2:views': 20,
    'article:3:views': 30,
}

for chave, incremento in contadores.items():
    pipe.incrby(chave, incremento)

pipe.execute()
print('Contadores atualizados')

Comparação de Desempenho do Pipeline

Código de Teste

PYTHON
import redis
import time

r = redis.Redis()

# Dados de teste
n = 10000

# Método 1: abordagem regular
inicio = time.time()
for i in range(n):
    r.set(f'key:{i}', f'value:{i}')
fim = time.time()
print(f'Regular: {fim - inicio:.2f} segundos')

# Método 2: pipelining
inicio = time.time()
pipe = r.pipeline()
for i in range(n):
    pipe.set(f'key:{i}', f'value:{i}')
pipe.execute()
fim = time.time()
print(f'Pipeline: {fim - inicio:.2f} segundos')

Resultados de Exemplo

Regular: 5.23 segundos
Pipeline: 0.51 segundos

Melhoria de desempenho: cerca de 10x

Melhores Práticas do Pipeline

1. Definir um Tamanho de Lote Razoável

PYTHON
# ❌ Enviar muitos comandos de uma vez
pipe = r.pipeline()
for i in range(100000):
    pipe.set(f'key:{i}', f'value:{i}')
pipe.execute()  # Pode usar muita memória

# ✅ Executar em lotes
tamanho_lote = 1000
for lote in range(0, 100000, tamanho_lote):
    pipe = r.pipeline()
    for i in range(lote, lote + tamanho_lote):
        pipe.set(f'key:{i}', f'value:{i}')
    pipe.execute()

2. Tratamento de Erros

PYTHON
import redis

r = redis.Redis()
pipe = r.pipeline()

pipe.set('key1', 'value1')
pipe.incr('key1')  # Isso vai falhar (key1 não é um número)
pipe.set('key2', 'value2')

try:
    resultados = pipe.execute()
    for i, resultado in enumerate(resultados):
        if isinstance(resultado, Exception):
            print(f'Comando {i} falhou: {resultado}')
except Exception as e:
    print(f'Execução do pipeline falhou: {e}')

3. Usar Transações para Atomicidade

PYTHON
import redis

r = redis.Redis()

# Usar transação quando a atomicidade for necessária
pipe = r.pipeline(transaction=True)

pipe.set('account:a', '100')
pipe.set('account:b', '50')

try:
    resultados = pipe.execute()
    print('Transação executada com sucesso')
except Exception as e:
    print(f'Transação falhou: {e}')

4. Monitorar Desempenho do Pipeline

PYTHON
import redis
import time

r = redis.Redis()

inicio = time.time()
pipe = r.pipeline()

for i in range(10000):
    pipe.set(f'key:{i}', f'value:{i}')

resultados = pipe.execute()
fim = time.time()

print(f'Tempo de execução: {fim - inicio:.2f} segundos')
print(f'QPS: {10000 / (fim - inicio):.0f}')

Limitações do Pipeline

1. Uso de Memória

PYTHON
# Pipeline armazena em cache todos os comandos e resultados
# Um lote muito grande usa muita memória

# Solução: executar em lotes

2. Não Atômico

PYTHON
# Comandos em um pipeline podem ser interrompidos por outros clientes
# Use transações quando a atomicidade for necessária

3. Não Pode Usar Resultados Intermediários

PYTHON
# Não pode usar o resultado de um comando anterior no pipeline

# ❌ Exemplo incorreto
pipe = r.pipeline()
pipe.incr('counter')
# Não pode obter o resultado de incr para comandos subsequentes
pipe.set('result', ???)  # Não pode usar o resultado de incr
pipe.execute()

# ✅ Solução: usar scripts Lua

❓ Perguntas Frequentes

P Qual é a diferença entre pipeline e transação?
R Pipeline reduz viagens de rede, mas não garante atomicidade. Transações garantem atomicidade e também reduzem viagens de rede.
P Quanta melhoria de desempenho o pipelining pode proporcionar?
R Depende da latência da rede. Maior latência significa maior melhoria. Tipicamente 5-10x.
P Qual tamanho de lote devo usar?
R Recomendado 100-1000 comandos por lote. Muito grande usa muita memória; muito pequeno dá melhoria mínima.
P O que acontece se um comando no pipeline falhar?
R Uma falha de comando único não afeta outros comandos. Verifique os resultados retornados para erros.
P Quando devo usar pipelining?
R Operações em lote, importação/exportação de dados, e qualquer cenário onde a melhoria de desempenho seja necessária.

📖 Resumo

📝 Atividades

  1. Pipeline básico: Use pipelining para definir 100 chaves em lote, compare o desempenho com a abordagem regular
  2. Operações em lote: Implemente obtenção em lote e exclusão em lote com pipelining
  3. Comparação de desempenho: Teste o desempenho com diferentes tamanhos de lote (10, 100, 1000)
  4. Pipeline + transação: Implemente operações em lote atômicas usando pipeline com transações

Próxima Lição

Na próxima lição, nós aprenderemos sobre Python com Redis, abordando operações Redis com Python.

100%