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

Lock Pessimista no Node.js

Atualizado em: 25 de agosto de 2026

Rack de servidores processando fluxos de dados no Node.js

O Lock Pessimista no Node.js protege dados concorridos bloqueando registros antes da alteração. Em vez de assumir que conflitos serão raros, a aplicação reserva explicitamente as linhas necessárias e impede que outra transação as modifique até o commit ou rollback.

Esse padrão é útil em estoque, reservas, filas, limites financeiros e recursos disputados. Ele reduz o risco de duas operações confirmarem sobre o mesmo estado, mas aumenta espera, possibilidade de deadlock e consumo de conexões. Por isso, a transação precisa ser curta, previsível e totalmente concentrada no banco.

Neste guia, você aprenderá a usar SELECT FOR UPDATE, NOWAIT, SKIP LOCKED, timeouts, ordem de bloqueio, transações com pg, retries, métricas e testes concorrentes.

O que é lock pessimista?

Lock pessimista presume que duas operações podem disputar o mesmo recurso. A primeira transação adquire o bloqueio, e as demais aguardam ou falham conforme a estratégia.

A documentação oficial de bloqueios explícitos do PostgreSQL descreve modos de lock e conflitos. A documentação de transações do node-postgres mostra como manter as queries na mesma conexão.

Para a base transacional, consulte Transações PostgreSQL no Node.js. Para comparar estratégias, veja Lock Otimista no Node.js.

SELECT FOR UPDATE

SELECT id, stock
FROM products
WHERE id = $1
FOR UPDATE;

A linha selecionada fica protegida contra atualizações concorrentes até o final da transação.

Exemplo com pg

const client = await pool.connect();

try {
  await client.query('BEGIN');

  const result = await client.query(`
    SELECT id, stock
    FROM products
    WHERE id = $1
    FOR UPDATE
  `, [productId]);

  const product = result.rows[0];

  if (!product || product.stock < quantity) {
    throw new Error('Estoque insuficiente');
  }

  await client.query(`
    UPDATE products
    SET stock = stock - $1
    WHERE id = $2
  `, [quantity, productId]);

  await client.query('COMMIT');
} catch (error) {
  await client.query('ROLLBACK');
  throw error;
} finally {
  client.release();
}

O mesmo client deve executar BEGIN, SELECT, UPDATE e COMMIT.

Por que não usar pool.query?

pool.query() pode selecionar outra conexão. A transação pertence à sessão que recebeu o BEGIN. Misturar conexões remove a garantia de atomicidade.

Veja Pool PostgreSQL no Node.js para gerenciamento correto de clientes.

FOR NO KEY UPDATE

Esse modo bloqueia alterações na linha, mas pode permitir certas operações que não modificam chaves usadas por foreign keys. Ele é adquirido automaticamente por alguns UPDATEs.

FOR SHARE

FOR SHARE impede atualizações conflitantes enquanto permite que outras transações também adquiram lock de compartilhamento.

FOR KEY SHARE

É um lock mais leve, usado para proteger chaves referenciadas sem bloquear toda alteração não relacionada.

Escolha o menor lock suficiente

Bloqueios mais fortes reduzem concorrência. Use o modo que realmente protege a invariante.

NOWAIT

SELECT id, stock
FROM products
WHERE id = $1
FOR UPDATE NOWAIT;

Em vez de aguardar, a query falha imediatamente se outra transação já possui um lock incompatível.

Quando usar NOWAIT?

  • interfaces que precisam responder rápido;
  • operações com fallback;
  • recursos que podem ser tentados depois;
  • processos que preferem erro a fila de espera.

SKIP LOCKED

SELECT id, payload
FROM jobs
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 10;

Linhas bloqueadas por outros workers são ignoradas. Isso permite que vários consumidores retirem trabalhos diferentes da mesma tabela.

Fila com SKIP LOCKED

const jobs = await client.query(`
  SELECT id, payload
  FROM jobs
  WHERE status = 'pending'
  ORDER BY priority DESC, id
  FOR UPDATE SKIP LOCKED
  LIMIT $1
`, [batchSize]);

await client.query(`
  UPDATE jobs
  SET status = 'processing',
      started_at = now(),
      worker_id = $1
  WHERE id = ANY($2::bigint[])
`, [workerId, jobs.rows.map(job => job.id)]);

A seleção e a marcação precisam ocorrer na mesma transação.

Recuperação de jobs abandonados

Se o worker cair depois de marcar processing, outro processo precisa recuperar trabalhos antigos:

UPDATE jobs
SET status = 'pending',
    worker_id = NULL,
    started_at = NULL
WHERE status = 'processing'
  AND started_at < now() - interval '10 minutes';

A operação deve ser idempotente e considerar jobs realmente longos.

Transações curtas

Não mantenha lock enquanto chama APIs externas, espera usuário, envia e-mail ou processa arquivo. Faça apenas o trabalho de banco necessário e confirme rapidamente.

Chamadas externas

Um pagamento remoto não participa da transação PostgreSQL. Use estados intermediários, idempotência e outbox em vez de segurar a linha durante uma chamada HTTP.

Consulte Idempotência em APIs Node.js.

Ordem consistente

Bloqueie múltiplos recursos sempre na mesma ordem:

const accountIds = [sourceId, destinationId]
  .sort((a, b) => a - b);

await client.query(`
  SELECT id
  FROM accounts
  WHERE id = ANY($1::bigint[])
  ORDER BY id
  FOR UPDATE
`, [accountIds]);

Se todas as transações seguem essa ordem, o risco de deadlock diminui.

Deadlock

Um deadlock ocorre quando A espera B e B espera A. O PostgreSQL detecta o ciclo e aborta uma transação com SQLSTATE 40P01.

Retry de deadlock

async function withDeadlockRetry(operation) {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    try {
      return await operation();
    } catch (error) {
      if (error.code !== '40P01' || attempt === 2) {
        throw error;
      }

      await delay(backoffWithJitter(attempt));
    }
  }
}

O callback não pode disparar efeitos externos que seriam repetidos.

lock_timeout

SET LOCAL lock_timeout = '2s';

A query falha se esperar além do prazo. Use SET LOCAL dentro da transação para não alterar futuras operações da conexão.

statement_timeout

SET LOCAL statement_timeout = '5s';

Limita a duração de cada comando. Um timeout aborta a transação e exige rollback.

idle_in_transaction_session_timeout

Esse parâmetro ajuda a encerrar sessões esquecidas em estado idle in transaction, que podem manter locks e snapshots por muito tempo.

Lock de tabela

Comandos DDL e algumas operações adquirem locks de tabela. Em APIs normais, prefira bloqueios de linha. Um lock amplo pode interromper grande parte do tráfego.

Advisory locks

Locks consultivos protegem recursos definidos pela aplicação que não correspondem diretamente a uma linha. Eles serão tratados em artigo próprio e não substituem constraints.

Constraint ainda é necessária

O lock protege a sequência, mas regras como estoque não negativo devem, quando possível, existir no banco:

ALTER TABLE products
ADD CONSTRAINT products_stock_nonnegative
CHECK (stock >= 0);

Update atômico

Às vezes, uma única query elimina a necessidade de leitura bloqueante:

UPDATE products
SET stock = stock - $1
WHERE id = $2
  AND stock >= $1
RETURNING stock;

Se nenhuma linha retorna, não havia estoque. Essa abordagem costuma ser mais eficiente.

Lock pessimista versus update condicional

Use lock quando precisa ler vários campos e tomar decisões complexas. Use update condicional quando a invariante cabe em uma única expressão SQL.

Isolamento

Locks funcionam em conjunto com o nível de isolamento. Em Read Committed, o SELECT bloqueante vê dados confirmados antes do comando e espera alterações concorrentes necessárias.

Serializable

Em Serializable, o PostgreSQL pode abortar transações mesmo sem deadlock físico para preservar uma ordem serial válida. Planeje retry para SQLSTATE 40001.

Autorização

Valide acesso antes de bloquear. Não mantenha lock enquanto busca dados externos de autorização.

Multi-tenant

Inclua o tenant no WHERE:

SELECT id, stock
FROM products
WHERE id = $1
  AND tenant_id = $2
FOR UPDATE;

Isso evita bloquear ou alterar recurso de outra organização.

Pagamentos

Bloquear uma linha de pedido pode impedir duas transições de status, mas não garante que o provedor não cobre duas vezes. Combine com chave de idempotência.

Reservas

Para reservar assento, bloqueie a linha, verifique disponibilidade, altere o status e confirme. Defina expiração para reservas abandonadas.

Contadores

Para incrementar um contador, uma única query atômica é melhor que SELECT FOR UPDATE seguido de UPDATE.

Observabilidade

Registre:

  • operação;
  • tempo de espera;
  • duração da transação;
  • timeout;
  • deadlock;
  • retry;
  • quantidade de linhas;
  • resultado.

Métricas

Crie histograms para duração e counters para timeouts e deadlocks. Veja Métricas Prometheus no Node.js.

Logs

Não registre SQL completo com dados sensíveis. Use operação lógica, SQLSTATE e IDs internos quando necessário.

pg_stat_activity

SELECT pid,
       state,
       wait_event_type,
       wait_event,
       now() - xact_start AS age,
       query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

Essa consulta ajuda a encontrar transações longas.

pg_locks

pg_locks mostra locks concedidos e aguardando. Combine com pg_stat_activity em um runbook de incidente.

Testes concorrentes

Use PostgreSQL real. Inicie duas conexões, bloqueie a mesma linha e confirme o comportamento de espera, NOWAIT ou timeout.

Teste de estoque

test('não reserva mais que o estoque', async () => {
  const attempts = Array.from({ length: 20 }, () =>
    reserveProduct(productId, 1)
  );

  await Promise.allSettled(attempts);

  const product = await findProduct(productId);
  assert.ok(product.stock >= 0);
});

Teste de deadlock

Adquira locks em ordens opostas em duas transações e confirme que a aplicação repete somente o erro apropriado.

Teste de timeout

Mantenha uma linha bloqueada, execute outra transação com lock_timeout curto e verifique o erro e o rollback.

Erros comuns

  • Usar pool.query: a transação se divide entre conexões.
  • Chamar API externa: o lock permanece por muito tempo.
  • Sem ordem consistente: deadlocks aumentam.
  • Sem timeout: requisições aguardam indefinidamente.
  • Retry de qualquer erro: falhas permanentes repetem.
  • Lock amplo: throughput cai.
  • Esquecer rollback: a conexão retorna suja ao pool.

Boas práticas

  • Use a mesma conexão.
  • Mantenha a transação curta.
  • Bloqueie na mesma ordem.
  • Defina lock_timeout.
  • Use NOWAIT ou SKIP LOCKED quando adequado.
  • Prefira update atômico quando possível.
  • Mantenha constraints.
  • Repita apenas erros transitórios.
  • Monitore espera e deadlocks.
  • Teste com concorrência real.

Conclusão

O Lock Pessimista no Node.js oferece controle direto sobre recursos disputados. Com SELECT FOR UPDATE, a aplicação impede que duas transações confirmem sobre o mesmo estado ao mesmo tempo.

O custo é contenção. Por isso, locks devem ser pequenos, curtos e adquiridos em ordem consistente. Com timeouts, constraints, retries limitados e métricas, o padrão protege estoque, reservas e filas sem transformar o banco em um gargalo permanente.

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