Como implementar distributed locking com Redis para evitar race conditions
1. Introdução ao Distributed Locking e Race Conditions
Em sistemas distribuídos, race conditions ocorrem quando múltiplos processos ou threads concorrentes acessam e modificam um recurso compartilhado sem coordenação adequada, levando a estados inconsistentes. Imagine dois workers processando pagamentos simultaneamente e debitando o mesmo saldo — sem controle, ambos poderiam considerar o saldo suficiente, gerando inconsistência financeira.
O distributed locking é uma técnica que garante que apenas um nó do sistema tenha acesso exclusivo a um recurso crítico em um determinado momento. Redis se destaca como escolha popular por sua baixa latência, operações atômicas e simplicidade de implementação. Com um lock distribuído baseado em Redis, podemos serializar o acesso a recursos compartilhados, prevenindo race conditions sem sacrificar desempenho.
2. Fundamentos do Redis para Locking
O Redis oferece comandos essenciais para implementar locks distribuídos:
- SET key value NX EX timeout: Comando atômico que define uma chave apenas se ela não existir (NX), com expiração automática (EX). Essa atomicidade é crucial para evitar condições de corrida durante a aquisição do lock.
- EXPIRE key seconds: Define um TTL para liberar automaticamente o lock em caso de falha.
- DEL key: Remove a chave para liberar o lock manualmente.
O comando SETNX (SET if Not eXists) era tradicionalmente usado, mas o comando SET com opções NX e EX é preferível por ser atômico, evitando a necessidade de múltiplas operações.
3. Implementação Básica de um Lock com Redis
A implementação mais simples utiliza o comando SET com opções NX e EX:
# Aquisição do lock
SET resource_lock "unique_identifier" NX EX 30
# Retorna OK se adquiriu, nil se já existe
# Liberação do lock
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Exemplo prático em pseudocódigo:
def acquire_lock(redis_client, lock_name, timeout_seconds):
identifier = generate_uuid()
success = redis_client.set(lock_name, identifier, nx=True, ex=timeout_seconds)
if success:
return identifier
return None
def release_lock(redis_client, lock_name, identifier):
script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
redis_client.eval(script, 1, lock_name, identifier)
O timeout (TTL) é fundamental como mecanismo de segurança contra deadlocks: se o processo que adquiriu o lock falhar, o lock será automaticamente liberado após o tempo configurado.
4. Lidando com Problemas de Consistência e Segurança
Um problema crítico ocorre quando o processo que adquiriu o lock trava antes de liberá-lo. Sem validação, outro processo poderia liberar acidentalmente o lock de outro. A solução é usar um identificador único:
# Script Lua para liberação segura
local lock_value = redis.call("GET", KEYS[1])
if lock_value == ARGV[1] then
redis.call("DEL", KEYS[1])
return 1
else
return 0
end
Esse script verifica se o valor armazenado corresponde ao identificador do proprietário antes de liberar, prevenindo que processos maliciosos ou atrasados removam locks alheios.
5. Redlock: Algoritmo para Alta Disponibilidade
Para ambientes críticos, o algoritmo Redlock proposto por Salvatore Sanfilippo (criador do Redis) oferece maior resiliência:
# Algoritmo Redlock simplificado
1. Obter timestamp atual T1
2. Tentar adquirir lock em N instâncias Redis independentes
3. Considerar lock adquirido se obteve em maioria (N/2 + 1) dentro do timeout
4. Tempo total gasto = T2 - T1
5. Se lock adquirido, tempo de validade efetivo = TTL - (T2 - T1)
6. Para liberar, deletar de todas as instâncias
# Exemplo com 5 instâncias Redis
instances = ["redis://node1:6379", "redis://node2:6379", "redis://node3:6379",
"redis://node4:6379", "redis://node5:6379"]
success_count = 0
start_time = current_time_millis()
for instance in instances:
if acquire_lock(instance, "resource", identifier, ttl=1000):
success_count += 1
elapsed = current_time_millis() - start_time
if elapsed > 1000:
break
if success_count >= 3 and elapsed <= 1000:
lock_validity = 1000 - elapsed
# Lock adquirido com sucesso
else:
# Falha na aquisição, liberar locks parciais
Trade-offs: Redlock oferece alta disponibilidade, mas assume que clocks dos servidores estão sincronizados. Em cenários com clock drift severo, pode haver inconsistências.
6. Padrões Avançados e Boas Práticas
Watchdog para renovação automática: Em operações longas, implemente um mecanismo que renove o TTL periodicamente:
def acquire_lock_with_watchdog(redis_client, lock_name, ttl=30, renewal_interval=10):
identifier = generate_uuid()
if not redis_client.set(lock_name, identifier, nx=True, ex=ttl):
return None
def renewal_loop():
while True:
sleep(renewal_interval)
redis_client.expire(lock_name, ttl)
thread = start_background_thread(renewal_loop)
return (identifier, thread)
Fences e leases: Utilize números de sequência (fences) para evitar problemas de ordem. Um fence token incrementa a cada aquisição, permitindo que o recurso rejeite operações com tokens desatualizados.
Bibliotecas maduras: Redisson (Java) e redis-py-lock (Python) implementam esses padrões de forma robusta, lidando com edge cases como falhas de rede e timeouts.
7. Casos de Uso e Exemplos Práticos
Sincronização de jobs agendados:
# Job que deve executar apenas em um worker
def process_daily_report():
lock_id = acquire_lock(redis, "report_job_lock", 300)
if not lock_id:
return # Outro worker já está processando
try:
generate_report()
send_emails()
finally:
release_lock(redis, "report_job_lock", lock_id)
Controle de estoque:
def reserve_product(product_id, quantity):
lock_name = f"stock_lock:{product_id}"
lock_id = acquire_lock(redis, lock_name, 5)
if not lock_id:
raise Exception("Recurso ocupado")
try:
current_stock = redis.get(f"stock:{product_id}")
if current_stock >= quantity:
redis.decrby(f"stock:{product_id}", quantity)
return True
return False
finally:
release_lock(redis, lock_name, lock_id)
Comparado a ZooKeeper ou etcd, Redis oferece menor latência e simplicidade, mas com garantias de consistência mais fracas. Para sistemas financeiros críticos, ZooKeeper pode ser mais adequado.
8. Monitoramento e Debug de Locks Distribuídos
Métricas essenciais para monitorar:
# Métricas via Redis INFO
- hit_rate: locks adquiridos com sucesso / total tentativas
- contention_rate: tentativas de lock que falharam por contenção
- avg_lock_time: tempo médio que locks permanecem ativos
- expiration_rate: locks que expiraram por timeout
# Logging estruturado
log_lock_event(
lock_name="resource_lock",
action="acquire" | "release" | "expire",
identifier="uuid-1234",
duration_ms=150,
success=True
)
Estratégias de fallback: implemente circuit breakers para evitar cascateamento de falhas e use locks com timeout decrescente para mitigar contenção excessiva.
Referências
-
Documentação Oficial do Redis sobre Distributed Locks — Guia completo com algoritmos, exemplos e considerações de segurança para implementação de locks distribuídos com Redis.
-
Algoritmo Redlock - Proposta Original de Salvatore Sanfilippo — Descrição detalhada do algoritmo Redlock para alta disponibilidade em múltiplas instâncias Redis.
-
Martin Kleppmann - How to do distributed locking — Análise crítica dos desafios de distributed locking, incluindo problemas de clock drift e a necessidade de fences.
-
Redisson - Distributed Locks and Synchronizers — Documentação da biblioteca Redisson para Java, implementando locks Redis com watchdog, Redlock e padrões avançados.
-
Redis-py-lock - Biblioteca Python para Locks Redis — Implementação Python de locks distribuídos com Redis, incluindo renovação automática e scripts Lua seguros.