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.
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?