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 30000NX 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 LOCKEDpara 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.



