Operações distribuídas e concorrência

Operações atômicas e sincronização

Domine operações atômicas no Redis — INCR, WATCH/MULTI/EXEC, Lua scripts, Redlock e padrões de sincronização distribuída.

Avançado 30 min 25 pontos Leitura 0%

Nesta aula você vai

  • Implementar operações atômicas com comandos nativos do Redis
  • Usar transações WATCH/MULTI/EXEC e Lua scripts para lógica complexa
  • Aplicar locks distribuídos (Redlock) para sincronização entre processos

Operações atômicas e sincronização

Objetivos

Nesta aula você vai:

  • Implementar operações atômicas com comandos nativos do Redis
  • Usar transações e Lua scripts para lógica complexa com garantias
  • Aplicar locks distribuídos para coordenação entre processos

Conteúdo

O problema da concorrência

Imagine dois caixas de supermercado vendendo o último item do estoque simultaneamente. Sem coordenação, ambos leem "1 unidade", ambos vendem, e o estoque fica negativo. Em sistemas distribuídos com múltiplas instâncias de aplicação, esse cenário é a regra — não a exceção.

Operações atômicas garantem que uma sequência de leitura-modificação-escrita execute sem interrupção — como se o universo pausasse para que uma única operação completasse antes da próxima começar.

Nível 1: comandos atômicos nativos

Redis oferece comandos que são atomicamente seguros por design:

# Incremento/decremento atômico
SET contador:visitas 0
INCR contador:visitas        # Retorna 1
INCR contador:visitas        # Retorna 2
INCRBY contador:visitas 10   # Retorna 12
DECR contador:visitas        # Retorna 11

# Operações atômicas em hash
HINCRBY carrinho:42 quantidade 1
HINCRBYFLOAT produto:1001 preco -5.50

# Set condicional (compare-and-set)
SET lock:recurso "worker-1" NX EX 30   # Só define se não existe
SET saldo:42 100 XX                     # Só define se já existe

# Get-and-set atômico
GETSET flag:processado "sim"   # Retorna valor antigo, define novo
Comando Atomicidade Caso de uso
INCR/DECR Total Contadores, rate limiting
HINCRBY Total Campos numéricos em hash
SET NX Total Lock distribuído
GETSET Total Swap de valor
LPUSH/RPOP Total Fila FIFO
SADD Total Conjunto sem duplicatas

Nível 2: transações MULTI/EXEC

Para combinar múltiplos comandos atomicamente:

# Reserva de estoque atômica
redis-cli WATCH estoque:produto:1001
redis-cli GET estoque:produto:1001
# Retorna "5"

redis-cli MULTI
redis-cli DECRBY estoque:produto:1001 1
redis-cli LPUSH pedidos:fila "pedido-1001"
redis-cli EXEC
# Retorna array com resultados, ou nil se WATCH detectou mudança

WATCH implementa optimistic locking:

Processo A: WATCH estoque → GET 5
Processo B: WATCH estoque → GET 5
Processo A: MULTI → DECRBY 1 → EXEC ✓ (estoque = 4)
Processo B: MULTI → DECRBY 1 → EXEC ✗ (WATCH falhou — estoque mudou)
Processo B: retry...
Aspecto MULTI/EXEC Pipeline
Atomicidade Sim (tudo ou nada) Não
Isolamento Sim (com WATCH) Não
Rollback EXEC falha se WATCH detecta mudança Sem rollback
Performance Menor (bloqueia durante MULTI) Maior

Nível 3: Lua scripts

Para lógica condicional complexa com atomicidade garantida:

# Transferência bancária atômica
redis-cli EVAL "
  local origem = KEYS[1]
  local destino = KEYS[2]
  local valor = tonumber(ARGV[1])

  local saldo_origem = tonumber(redis.call('GET', origem) or '0')
  if saldo_origem < valor then
    return redis.error_reply('saldo_insuficiente')
  end

  redis.call('DECRBY', origem, valor)
  redis.call('INCRBY', destino, valor)
  return saldo_origem - valor
" 2 conta:42 conta:99 100

Vantagens do Lua:

  • Atomicidade total (script executa sem interrupção no single-thread)
  • Lógica condicional (if/else) impossível com MULTI/EXEC
  • Menos round-trips de rede
  • Cache via SCRIPT LOAD + EVALSHA
# Carregar script uma vez
redis-cli SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Retorna SHA: a1b2c3d4e5f6...

# Executar por SHA (eficiente)
redis-cli EVALSHA a1b2c3d4e5f6... 1 minha:chave

Locks distribuídos

Lock simples com SET NX

# Adquirir lock
SET lock:pedido:1001 "worker-42" NX EX 30
# OK = adquirido | (nil) = já existe

# Trabalho protegido...
# Atualizar pedido no banco de dados

# Liberar lock (apenas o dono!)
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 simples:

Problema Cenário Consequência
Lock expira antes do trabalho Processo lento (> TTL) Outro processo adquire lock — execução dupla
Liberação por processo errado Sem verificação de dono Lock removido prematuramente
Falha após adquirir Processo morre Lock expira por TTL (OK) ou fica preso (sem TTL)

Redlock: lock com quorum

Algoritmo de Martin Kleppmann (antirez) para locks em múltiplas instâncias:

1. Obter timestamp atual (T1)
2. Tentar SET NX em N instâncias independentes (quorum = N/2 + 1)
3. Calcular tempo gasto (T2 - T1)
4. Se quorum atingido E tempo restante > 0 → lock adquirido
5. Ao liberar: DEL em todas as instâncias (com verificação de dono)
# Implementação via biblioteca (ex.: Redisson Java)
# Não implemente Redlock manualmente — use biblioteca testada

# Configuração conceitual:
# 3 instâncias Redis independentes
# Quorum = 2
# TTL = 30s
# Auto-renovação do lock enquanto processo vivo

Padrões de sincronização

Rate limiting atômico

# Sliding window com INCR + EXPIRE
redis-cli EVAL "
  local key = KEYS[1]
  local limit = tonumber(ARGV[1])
  local window = tonumber(ARGV[2])
  local current = tonumber(redis.call('GET', key) or '0')

  if current >= limit then
    return 0
  end

  if current == 0 then
    redis.call('SET', key, 1, 'EX', window)
  else
    redis.call('INCR', key)
  end
  return 1
" 1 rate:ip:192.168.1.1 100 60
# Retorna 1 = permitido, 0 = bloqueado

Semáforo distribuído

# Limitar concorrência a N processos
redis-cli EVAL "
  local key = KEYS[1]
  local max = tonumber(ARGV[1])
  local current = tonumber(redis.call('GET', key) or '0')

  if current >= max then
    return 0
  end

  redis.call('INCR', key)
  return 1
" 1 semaforo:importacao 5

Idempotência com SET NX

# Garantir processamento único de evento
SET evento:evt-12345:processado 1 NX EX 86400
# OK = primeiro processamento
# (nil) = já processado — ignorar

Comparação de mecanismos

Mecanismo Complexidade Garantia Performance
INCR/DECR Baixa Atômica (single key) Excelente
SET NX Baixa Atômica (lock simples) Excelente
WATCH/MULTI/EXEC Média Otimista (retry em conflito) Boa
Lua script Média Atômica (lógica complexa) Boa
Redlock Alta Quorum (tolerância a falhas) Moderada

Armadilhas comuns

# ERRADO: GET + SET (race condition)
# valor = GET estoque:1001
# SET estoque:1001 (valor - 1)

# CORRETO: DECRBY atômico
DECRBY estoque:1001 1

# ERRADO: DEL lock sem verificar dono
DEL lock:recurso

# CORRETO: Lua com verificação
EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) end" 1 lock:recurso worker-42

# ERRADO: lock sem TTL (deadlock se processo morre)
SET lock:recurso "worker-42" NX

# CORRETO: lock com TTL
SET lock:recurso "worker-42" NX EX 30

Resumo

  • Comandos nativos (INCR, SET NX) resolvem 80% dos casos de concorrência
  • WATCH/MULTI/EXEC oferece optimistic locking; Lua scripts permitem lógica condicional atômica
  • Locks distribuídos exigem TTL + verificação de dono na liberação
  • Redlock com quorum para cenários que exigem tolerância a falha de instância

Para refletir

Em qual parte do seu sistema um GET seguido de SET poderia causar race condition? Qual comando atômico resolveria?