Gerencie atualizações concorrentes sem condições de corrida

Oi a todos,
Estou usando PostgreSQL com n8n e estou tentando entender a melhor forma de lidar com múltiplos workflows atualizando o mesmo registro ao mesmo tempo.
Por exemplo, duas execuções de webhook poderiam tentar atualizar a mesma linha simultaneamente:
UPDATE orders
SET status = ‘processed’
WHERE order_id = 1001;
Minha preocupação é evitar problemas como:
Atualizações perdidas
Condições de corrida
Processamento duplicado
Dados inconsistentes
Estou lendo sobre bloqueio em nível de linha (FOR UPDATE), bloqueio otimista e transações, mas não tenho certeza de qual abordagem funciona melhor em um ambiente n8n de produção.
Para quem está rodando PostgreSQL com workflows de alta concorrência:
• Vocês contam apenas com transações, ou também usam bloqueios em nível de linha?
• Quando vocês escolheriam bloqueio otimista em vez de bloqueio pessimista?
• Alguém experimentou deadlocks e como vocês lidaram com eles?
• Alguma dica de produção para manter os dados consistentes sem prejudicar o desempenho?
Gostaria de saber o que funcionou bem para outros em implementações reais do n8n.

Descrever o problema/erro/pergunta

Qual é a mensagem de erro (se houver)?

Por favor, compartilhe seu workflow

(Selecione os nós em sua tela e use os atalhos de teclado CMD+C/CTRL+C e CMD+V/CTRL+V para copiar e colar o workflow.)

Compartilhe a saída retornada pelo último nó

Informações sobre sua configuração n8n

  • Versão do n8n:
  • Banco de dados (padrão: SQLite):
  • Configuração do n8n EXECUTIONS_PROCESS (padrão: own, main):
  • Executando n8n via (Docker, npm, n8n cloud, aplicativo desktop):
  • Sistema operacional:

A melhor abordagem depende da frequência com que os mesmos registros são atualizados, mas para a maioria dos fluxos de trabalho de alta concorrência, transações e bloqueio em nível de linha funcionam bem juntos.

Abordagem recomendada

Use uma transação e bloqueie a linha antes de atualizá-la:
BEGIN;

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE;

UPDATE orders
SET status = ‘processed’
WHERE order_id = 1001;

COMMIT;

Isso impede que outra transação modifique a mesma linha até que a atual seja concluída.

Para sistemas de alto volume
Mantenha transações curtas
Indexe colunas consultadas com frequência
Use bloqueio otimista se conflitos de atualização forem raros

Oi @Keira_Becky
Para uma simples troca de status como no seu exemplo, pule o lock explícito e deixe o UPDATE ser o guardião. Uma única instrução atômica que só toca na linha se ela ainda não tiver sido processada:

UPDATE orders
SET status = 'processed'
WHERE order_id = 1001 AND status <> 'processed'
RETURNING order_id;

A primeira execução atualiza a linha, a segunda não encontra nenhuma linha então RETURNING retorna vazio, e você verifica “recebi uma linha de volta” para saber se já foi tratada. Isso elimina atualizações perdidas e processamento duplicado em uma única instrução, sem necessidade de transação multi-etapa.

Se você realmente precisar de um read-modify-write com um lock explícito, ele tem que rodar dentro de um único nó Execute Query, ou ativar a opção Transaction do nó. Nós Postgres separados abrem suas próprias conexões, então um lock adquirido em um nó é liberado antes do próximo nó rodar, e o lock não faz nada.

Para deadlocks com workers concorrentes puxando um lote, use SKIP LOCKED para que cada worker pegue linhas diferentes em vez de bloquear na mesma:

SELECT order_id
FROM orders
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 100;

Então ative Retry On Fail no nó para que um erro de serialização transitório apenas tente novamente.

Oi @Keira_Becky

Em qualquer fluxo de trabalho n8n que envolva PostgreSQL, sempre envolva as ações de banco de dados em uma única transação. No n8n, isso é feito com um nó “Start Transaction”, as instruções SELECT/UPDATE necessárias e um nó “Commit Transaction” (ou Rollback em caso de erro). As transações garantem que uma falha anule todo o conjunto de mudanças, prevenindo atualizações parciais e mantendo os dados consistentes mesmo se um fluxo de trabalho falhar.

Bloqueio pessimista (SELECT … FOR UPDATE ou FOR UPDATE SKIP LOCKED) é ideal quando você precisa de processamento exatamente uma vez, quando muitos workers competem pelas mesmas poucas linhas, ou quando uma lógica toca múltiplas linhas que devem permanecer sincronizadas. O bloqueio é mantido até a transação ser confirmada, garantindo que nenhum outro fluxo de trabalho possa ler ou modificar a linha bloqueada, o que elimina atualizações perdidas e processamento duplicado.

Bloqueio otimista funciona melhor com baixa contenção. Ao adicionar uma coluna version (ou updated_at) e atualizar com uma condição como WHERE version = $oldVersion, você permite que workers concorrentes tentem a atualização; apenas o primeiro consegue, e os outros detectam um conflito (zero linhas afetadas) e podem tentar novamente. Essa abordagem evita a sobrecarga de bloqueios e é útil para atualizações em lote ou execuções de webhook sem estado, onde um simples loop de repetição é suficiente.

Mesmo com bloqueio cuidadoso, deadlocks podem ocorrer quando fluxos de trabalho bloqueiam linhas em ordens diferentes. Mitigue-os sempre adquirindo bloqueios em uma ordem determinística (por exemplo, ORDER BY order_id ASC), usando SKIP LOCKED para processamento estilo fila, e implementando lógica de repetição para o erro de deadlock 40P01. Monitorar configurações como log_lock_waits = on ajuda a identificar incidentes de deadlock rapidamente.

Para manter o desempenho alto, mantenha as transações curtas, evite chamadas HTTP externas dentro de uma transação e garanta que as colunas relevantes (order_id, status, version) sejam indexadas. Se muitas linhas precisam da mesma alteração, agrupe-as em uma única instrução UPDATE em vez de gerar um fluxo de trabalho separado por linha. Usar um pool de workers limitado e SKIP LOCKED reduz a contenção de bloqueios e evita que workers fiquem ociosos enquanto aguardam um bloqueio.

Use bloqueio otimista quando a contenção é rara e você pode tolerar repetições; mude para bloqueio pessimista quando você deve garantir acesso single-thread ou quando múltiplas linhas estão envolvidas em uma regra de negócio. Para processamento estilo fila, FOR UPDATE SKIP LOCKED combinado com um curto loop de repetição/back-off é o padrão de produção mais comum no n8n. Seguir essas diretrizes gera fluxos de trabalho consistentes em dados e de alta taxa de transferência sem sacrificar o desempenho.

Muito obrigado @Niffzy @Anshul_Namdev @kjooleng pela explicação detalhada

A abordagem atômica UPDATE … RETURNING faz sentido para transições de status simples, enquanto transações e padrões de bloqueio se tornam importantes quando múltiplas alterações relacionadas precisam acontecer juntas. Isso esclareceu quando usar cada abordagem em fluxos de trabalho de produção do n8n. Agradeço os insights