Distributed Locking: além do Redlock simples
Como implementar locks distribuídos robustos em produção com etcd, Consul e padrões avançados.
Resumo executivo
Como implementar locks distribuídos robustos em produção com etcd, Consul e padrões avançados.
Ultima atualizacao: 12/03/2026
Introdução: A mentira de "lock simples" em sistemas distribuídos
Em sistemas distribuídos, locks não são simples. Um lock que funciona em single-process (mutex, semaphore, monitor) falha catastroficamente quando estendido para múltiplas máquinas. Redes falham, relógios desincronizam, nós travam e messages são entregues fora de ordem.
O padrão Redlock (popularizado pelo Redis) é frequentemente implementado incorretamente. Martin Kleppmann em seu artigo "How to do distributed locking" demonstrou que implementações simplificadas de Redlock podem levar a race conditions e locks duplicados.
Para sistemas em produção em 2026, a decisão não é "se usar locking distribuído", mas "qual algoritmo e qual store de dados oferece as garantias corretas para meu caso de uso".
O problema de locking distribuído
Por que locks locais não funcionam
Em sistemas multi-instância, locks na memória de cada processo são insuficientes:
typescript// PROBLEMA: Lock local não protege entre instâncias
class InMemoryLock {
private locked = false;
async acquire(): Promise<boolean> {
if (this.locked) return false;
this.locked = true;
return true;
}
release(): void {
this.locked = false;
}
}
// Instância A: lock.acquire() → true
// Instância B: lock.acquire() → true (MAS DEVERIA SER FALSE!)
// Race condition: ambas as instâncias executam código críticoA garantia de mutual exclusão
Um lock distribuído correto deve garantir:
- Mutual Exclusion: No máximo um processo pode segurar o lock
- Freedom from Deadlock: O lock eventualmente é liberado mesmo se o processo segurar morrer
- Liveness: Se o lock estiver disponível, um processo deve poder adquiri-lo
- Fairness (opcional): Primeiro a pedir, primeiro a receber
Padrões de implementação
Padrão 1: Leases com TTL
A abordagem mais simples é usar leases com time-to-live (TTL). Se o processo morrer, o lease expira automaticamente:
typescriptinterface Lease {
acquire(key: string, ttl: number): Promise<LeaseToken>;
release(token: LeaseToken): Promise<void>;
renew(token: LeaseToken, ttl: number): Promise<void>;
}
async function withLease<T>(
lease: Lease,
key: string,
fn: () => Promise<T>
): Promise<T> {
const token = await lease.acquire(key, 30000); // 30 segundos
const renewInterval = setInterval(() => {
lease.renew(token, 30000);
}, 15000); // Renovar a cada 15s
try {
return await fn();
} finally {
clearInterval(renewInterval);
await lease.release(token);
}
}Trade-offs:
- ✅ Simples de implementar
- ✅ Protege contra processos mortos
- ❌ Se processo crasha antes do TTL expirar, lock fica preso
- ❌ Race condition se TTL expira mas processo ainda está rodando
Padrão 2: Fencing Tokens (Martín Kleppmann)
Fencing tokens resolvem o problema de locks expirados adicionando um número sequencial que cresce sempre:
typescriptinterface DistributedLock {
acquire(): Promise<{ token: number; lease: Lease }>;
release(token: number): Promise<void>;
}
async function withFencing<T>(
lock: DistributedLock,
resource: string,
fn: (fencingToken: number) => Promise<T>
): Promise<T> {
const { token, lease } = await lock.acquire();
try {
return await fn(token);
} finally {
await lease.release();
}
}
// USO: O recurso verifica se token é maior que o último visto
class StorageSystem {
private lastFencingToken = 0;
async write(data: string, fencingToken: number): Promise<void> {
if (fencingToken < this.lastFencingToken) {
throw new Error('Request from expired lock - ignoring');
}
this.lastFencingToken = fencingToken;
// Executar write
}
}Por que funciona:
- Se lock expira e novo lock é adquirido com token 101, qualquer request com token 100 é rejeitado
- O recurso (banco de dados, sistema de arquivos) valida o token antes de executar
- Garante que processos com locks expirados não podem causar dano
Padrão 3: Redlock (Multi-instance Redis)
Redlock usa múltiplas instâncias de Redis para garantir que lock é distribuído:
typescriptclass Redlock {
constructor(
private clients: Redis[],
private quorum: number
) {}
async acquire(
key: string,
ttl: number
): Promise<Lock | null> {
const value = crypto.randomUUID();
const startTime = Date.now();
// Tentar lock em todas as instâncias
const successes = await Promise.allSettled(
this.clients.map(client =>
client.set(key, value, 'PX', ttl, 'NX')
)
);
const acquiredCount = successes.filter(
s => s.status === 'fulfilled' && s.value === 'OK'
).length;
if (acquiredCount < this.quorum) {
// Falhou, liberar locks adquiridos
await this.unlockAll(key, value);
return null;
}
// Verificar se tempo de lock não excedeu TTL
const elapsed = Date.now() - startTime;
if (elapsed >= ttl) {
await this.unlockAll(key, value);
return null;
}
return { key, value, startTime };
}
private async unlockAll(key: string, value: string): Promise<void> {
await Promise.all(
this.clients.map(client =>
client.eval(
'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
1,
key,
value
)
)
);
}
}Trade-offs:
- ✅ Alta performance (memória-based)
- ✅ Disponibilidade com quorum
- ❌ Complexo de implementar corretamente
- ❌ Depende de relógios sincronizados entre instâncias Redis
- ❌ Race conditions se TTL expira e lock é re-adquirido rapidamente
Padrão 4: etcd Leases com Raft
etcd usa o algoritmo de consenso Raft para oferecer locks com garantias fortes:
typescriptimport { Lock, Etcd3 } from 'etcd3';
const client = new Etcd3({ hosts: 'localhost:2379' });
const lock = new Lock(
client,
'my-distributed-lock',
{
ttl: 30, // segundos
// etcd garante que lock é mantido com heartbeats
}
);
async function withEtcdLock<T>(fn: () => Promise<T>): Promise<T> {
await lock.acquire();
try {
return await fn();
} finally {
await lock.release();
}
}Vantagens do etcd:
- Garantias fortes de consenso via Raft
- Automatic lease renewal (sem race condition de TTL)
- Discovery service integrado
- Watch API para notificações
Padrão 5: Consul Sessions
Consul usa sessions com TTL para locking distribuído:
typescriptimport Consul from 'consul';
const consul = new Consul();
async function withConsulLock<T>(key: string, fn: () => Promise<T>): Promise<T> {
const session = await consul.session.create({
TTL: '30s',
Behavior: 'delete',
});
try {
// Tentar adquirir lock
const locked = await consul.lock.acquire({
key: `service/lock/${key}`,
session: session.ID,
});
if (!locked) {
throw new Error('Failed to acquire lock');
}
return await fn();
} finally {
await consul.session.destroy(session.ID);
}
}Vantagens do Consul:
- Integrado com service discovery
- Health checks nativos
- KV store com versioning
- Multi-datacenter support
Quando usar locking distribuído
Casos de uso apropriados
- Coordenação de jobs agendados: Garantir que apenas uma instância execute um cron job
- Resource allocation: Limitar acesso a recursos escassos (API quotas, hardware)
- Leader election: Designar líder para cluster de processamento
- Mutação de estado compartilhado: Modificar dados que não podem ser atomically updated
Casos de uso inapropriados
- Alta frequência de lock/unlock: Locking distribuído é caro (network round-trip)
- Long-held locks: Locks longos aumentam chance de timeout e race conditions
- Performance-critical paths: Se latência de lock é crítica, redesenhe arquitetura
Alternativas a locking
Idempotência
Ao invés de lock, torne operações idempotentes:
typescript// SEM LOCK: operação idempotente
async function createPayment(paymentId: string, amount: number) {
const existing = await db.payments.find({ id: paymentId });
if (existing) {
return existing; // Já processado, retornar resultado
}
return await db.payments.insert({ id: paymentId, amount });
}Optimistic Concurrency Control
Use versioning para detectar conflitos:
typescriptasync function updateDocument(
docId: string,
expectedVersion: number,
updates: Partial<Document>
): Promise<void> {
const result = await db.documents.updateOne(
{ id: docId, version: expectedVersion },
{ $set: { ...updates, version: expectedVersion + 1 } }
);
if (result.modifiedCount === 0) {
throw new Error('Document was modified by another process');
}
}Message Queue com Exactly-Once
Use message queue com garantias exactly-once:
typescript// Usar Kafka com idempotent consumer
consumer.subscribe({ topic: 'orders' });
await consumer.run({
eachMessage: async ({ message }) => {
const orderId = message.key.toString();
// Processar idempotentemente
await processOrder(orderId, message.value);
},
});Checklist de implementação
- [ ] Lock tem TTL/lease para lidar com processos mortos
- [ ] Lock release é idempotente (pode ser chamado múltiplas vezes)
- [ ] Usa fencing tokens se race conditions de TTL são aceitáveis
- [ ] Lock acquisition tem timeout (não espera indefinidamente)
- [ ] Implementa retry com backoff exponencial
- [ ] Monitora lock wait time e lock duration
- [ ] Tem fallback ou alerta se lock falha consistentemente
Conclusão
Distributed locking em 2026 é uma ferramenta madura com múltiplas implementações válidas: etcd (consenso Raft), Consul (sessions), Redis (Redlock), e custom solutions com fencing tokens.
A decisão de arquitetura deve ser baseada em:
- Garantias de consistência: Você tolera race conditions ocasionais?
- Performance: Qual latência de lock acquisition é aceitável?
- Complexidade operacional: Sua equipe pode operar etcd/Consul?
- Escalabilidade: Quantas instâncias competem pelo lock?
Para a maioria dos casos de uso, começar com etcd ou Consul oferece melhor balanço de garantias fortes e complexidade gerenciável. Redlock é apropriado quando performance é crítica e você entende as trade-offs de consistência.
Alternativas como idempotência e optimistic concurrency devem ser consideradas primeiro — muitas vezes o problema pode ser resolvido sem locking distribuído.
Precisa implementar locking distribuído ou redesenhar arquitetura para eliminar necessidade de locks? Fale com especialistas da Imperialis em web para projetar e implementar solução.