Knowledge

Distributed Locking: além do Redlock simples

Como implementar locks distribuídos robustos em produção com etcd, Consul e padrões avançados.

12/03/20267 min de leituraKnowledge
Distributed Locking: além do Redlock simples

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ítico

A garantia de mutual exclusão

Um lock distribuído correto deve garantir:

  1. Mutual Exclusion: No máximo um processo pode segurar o lock
  2. Freedom from Deadlock: O lock eventualmente é liberado mesmo se o processo segurar morrer
  3. Liveness: Se o lock estiver disponível, um processo deve poder adquiri-lo
  4. 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

  1. Coordenação de jobs agendados: Garantir que apenas uma instância execute um cron job
  2. Resource allocation: Limitar acesso a recursos escassos (API quotas, hardware)
  3. Leader election: Designar líder para cluster de processamento
  4. Mutação de estado compartilhado: Modificar dados que não podem ser atomically updated

Casos de uso inapropriados

  1. Alta frequência de lock/unlock: Locking distribuído é caro (network round-trip)
  2. Long-held locks: Locks longos aumentam chance de timeout e race conditions
  3. 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.

Fontes

Leituras relacionadas