Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Locks Distribuídos com Redis

Atualizado em: 16 de setembro de 2026

Rack de servidores processando fluxos de dados no Node.js

Os locks distribuídos com Redis coordenam processos que precisam acessar um recurso compartilhado de forma exclusiva. Em aplicações Node.js com várias réplicas, um mutex em memória protege apenas uma instância. Duas réplicas ainda podem executar o mesmo job, renovar o mesmo token ou processar a mesma tarefa simultaneamente.

Redis oferece comandos atômicos e expiração de chaves, permitindo criar uma lease temporária. Entretanto, um lock distribuído não é apenas um SETNX. É necessário usar um valor único, TTL, liberação condicional, retries com jitter e, para operações críticas, fencing tokens. Também é importante reconhecer limites de failover, relógios e pausas do processo.

Neste guia, você aprenderá a adquirir e liberar locks com segurança, usar a biblioteca Redlock, estender uma lease, evitar remoção do lock de outro cliente e decidir quando usar banco de dados, fila ou eleição de líder no lugar do Redis.

Quando um lock distribuído é útil?

  • Executar um job periódico em apenas uma réplica.
  • Evitar regeneração simultânea do mesmo relatório.
  • Proteger atualização rara de um recurso compartilhado.
  • Coordenar renovação de credenciais ou cache.
  • Serializar uma tarefa que não possui constraint adequada.

Locks não devem substituir idempotência e constraints de banco. Sempre que possível, proteja a regra na camada que armazena o dado.

Aquisição correta em uma instância

A documentação oficial de locks distribuídos com Redis recomenda adquirir uma chave com NX, expiração e valor aleatório:

SET lock:report:42 random-token NX PX 30000

NX cria somente quando a chave não existe. PX define o TTL em milissegundos. O token identifica o proprietário daquela aquisição.

Exemplo com node-redis

import { createClient } from 'redis';
import { randomUUID } from 'node:crypto';

const redis = createClient({
  url: process.env.REDIS_URL
});

await redis.connect();

async function acquireLock(resource, ttlMs) {
  const token = randomUUID();
  const key = `lock:${resource}`;

  const result = await redis.set(key, token, {
    NX: true,
    PX: ttlMs
  });

  return result === 'OK' ? { key, token, ttlMs } : null;
}

Não use um valor constante como locked. Cada tentativa precisa de um token diferente.

Liberação condicional

Este código é perigoso:

await redis.del(lock.key);

Se o trabalho demorou além do TTL, outra instância pode ter adquirido o mesmo lock. O primeiro cliente então excluiria o lock novo. Use comparação atômica:

const releaseScript = `
  if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
  end
  return 0
`;

async function releaseLock(lock) {
  return redis.eval(releaseScript, {
    keys: [lock.key],
    arguments: [lock.token]
  });
}

Em versões novas do Redis, comandos condicionais equivalentes podem estar disponíveis. Verifique a versão do servidor antes de depender deles.

Uso com try/finally

const lock = await acquireLock('report:42', 30_000);

if (!lock) {
  throw new Error('Recurso ocupado');
}

try {
  await generateReport(42);
} finally {
  await releaseLock(lock);
}

O finally reduz o tempo de espera quando o trabalho termina normalmente. O TTL continua sendo necessário para falhas do processo.

Escolhendo o TTL

O TTL precisa ser maior que a duração esperada, incluindo picos, pausas de garbage collection e latência. Um valor curto permite dois proprietários. Um valor muito longo aumenta indisponibilidade após falha.

Meça p99 do trabalho e aplique margem. Para tarefas altamente variáveis, use extensão limitada ou divida o processamento em etapas menores.

Extensão do lock

A extensão também deve verificar o token:

const extendScript = `
  if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('pexpire', KEYS[1], ARGV[2])
  end
  return 0
`;

async function extendLock(lock, ttlMs) {
  const result = await redis.eval(extendScript, {
    keys: [lock.key],
    arguments: [lock.token, String(ttlMs)]
  });

  if (result !== 1) {
    throw new Error('Lock perdido');
  }
}

Limite o número de extensões. Não suponha que o lock continua válido apenas porque o processo está vivo.

Retries com jitter

async function acquireWithRetry(resource, options) {
  for (let attempt = 0; attempt < options.attempts; attempt++) {
    const lock = await acquireLock(resource, options.ttlMs);
    if (lock) return lock;

    const base = Math.min(2000, 100 * 2 ** attempt);
    const delay = Math.floor(base * (0.5 + Math.random()));
    await new Promise(resolve => setTimeout(resolve, delay));
  }

  throw new Error('Não foi possível adquirir o lock');
}

O jitter evita que várias instâncias repitam ao mesmo tempo. Consulte Retry com Backoff no Node.js.

Biblioteca Redlock

Uma implementação conhecida é node-redlock:

import Redlock from 'redlock';

const redlock = new Redlock([redis], {
  driftFactor: 0.01,
  retryCount: 5,
  retryDelay: 200,
  retryJitter: 100,
  automaticExtensionThreshold: 500
});

await redlock.using(
  ['locks:report:42'],
  10_000,
  async signal => {
    if (signal.aborted) throw signal.error;
    await generateReport(42);
  }
);

Fixe a versão, leia as garantias e teste falhas de rede. Uma biblioteca reduz erros de implementação, mas não muda as limitações do problema distribuído.

Redlock com vários nós

O algoritmo Redlock tenta adquirir o lock na maioria de instâncias Redis independentes dentro do prazo da lease. Ele foi proposto para reduzir o risco de uma falha simples de master e promoção de réplica violar exclusão mútua.

Há debate técnico sobre garantias sob pausas, clock drift e partições. Para dados críticos, use fencing tokens e um armazenamento que rejeite operações antigas.

Fencing tokens

Um fencing token é um número monotônico entregue a cada aquisição:

const fencingToken = await redis.incr('lock-sequence:report:42');

O recurso protegido registra o maior token aceito. Uma instância pausada com token 41 não pode escrever depois que outra instância usou 42.

UPDATE reports
SET content = $1, fencing_token = $2
WHERE id = $3
  AND fencing_token < $2;

O lock reduz concorrência; o fencing token protege contra proprietário atrasado.

Locks e banco de dados

Para registros PostgreSQL, prefira recursos nativos quando o trabalho acontece na mesma transação:

  • SELECT ... FOR UPDATE;
  • advisory locks;
  • constraints únicas;
  • SKIP LOCKED para filas.

Veja Lock Pessimista no Node.js e Advisory Locks no Node.js.

Locks e idempotência

Mesmo com lock, retries e falhas podem repetir trabalho. Use uma chave idempotente e registre resultado. Consulte Idempotência em APIs Node.js.

Jobs periódicos

async function runScheduledJob() {
  const lock = await acquireLock('daily-cleanup', 5 * 60_000);
  if (!lock) return;

  try {
    await cleanupExpiredData();
  } finally {
    await releaseLock(lock);
  }
}

Se o job pode durar mais, divida em lotes, registre progresso e use extensão segura.

Cache stampede

Um lock por chave pode impedir várias instâncias de recomputar o mesmo cache. Consumidores que não adquirem podem aguardar brevemente, servir valor stale ou retornar fallback. O próximo artigo aprofunda esse padrão.

Observabilidade

Meça:

  • tempo para adquirir;
  • taxa de sucesso e contenção;
  • extensões;
  • locks perdidos;
  • duração do trabalho;
  • liberações rejeitadas;
  • TTL restante;
  • falhas do Redis.

Não use o resource ID como label de alta cardinalidade em métricas.

Testes de falha

Cubra:

  • duas instâncias concorrentes;
  • processo encerrado sem liberar;
  • trabalho maior que o TTL;
  • extensão falhando;
  • Redis indisponível;
  • cliente pausado e retomado;
  • liberação com token errado;
  • fencing token antigo.

Erros comuns

  • SETNX sem TTL: crash causa deadlock.
  • DEL direto: remove lock de outro cliente.
  • TTL menor que o trabalho: dois proprietários.
  • Retry sem jitter: contenção sincronizada.
  • Lock como única proteção: pausa pode gerar escrita atrasada.
  • Failover assíncrono ignorado: exclusão pode ser violada.
  • Sem idempotência: retries repetem efeitos.
  • Lock global: paralelismo desnecessariamente baixo.

Conclusão

Os locks distribuídos com Redis são úteis para coordenação temporária entre réplicas Node.js. A implementação mínima segura usa SET NX PX, token único e liberação condicional.

Escolha TTL com métricas, use retries com jitter, limite extensões e adicione fencing tokens quando consistência importa. Para regras armazenadas em banco, prefira constraints e locks nativos. Um lock deve reduzir concorrência, não ser a única barreira contra efeitos duplicados.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita