Atualizações simultâneas no n8n quando múltiplos workflows escrevem no SQL Server

Oi PESSOAL, estou usando n8n com Microsoft SQL Server, e múltiplas execuções de workflow podem atualizar o mesmo registro de cliente quase ao mesmo tempo.
Estou preocupado com race conditions e atualizações perdidas quando duas execuções leem o mesmo registro e depois tentam atualizá-lo.
UPDATE Customers
SET
Status = @status,
UpdatedAt = GETUTCDATE()
WHERE CustomerId = @customerId;
Você recomendaria transações, UPDLOCK/ROWLOCK, concorrência otimista usando uma coluna rowversion, ou uma combinação? Além disso, como você estruturaria o workflow n8n para que uma atualização falha ou concorrente possa ser retentada com segurança sem sobrescrever dados mais recentes?

Descreva o problema/erro/pergunta

Qual é a mensagem de erro (se houver)?

Por favor, compartilhe seu workflow

(Selecione os nós no seu canvas 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 n8n:
  • Banco de dados (padrão: SQLite):
  • Configuração n8n EXECUTIONS_PROCESS (padrão: own, main):
  • Executando n8n via (Docker, npm, n8n cloud, aplicativo desktop):
  • Sistema operacional:

Oi @Mary_Berry

Use concorrência otimista com a rowversion do SQL Server, especialmente quando múltiplas execuções do n8n podem atualizar o mesmo registro.
Adicione uma coluna rowversion:
ALTER TABLE Customers
ADD RowVersion ROWVERSION;

Quando o n8n lê o cliente, mantenha a RowVersion atual. Depois atualize o registro apenas se essa versão ainda estiver inalterada:
UPDATE Customers
SET
Status = @status,
UpdatedAt = SYSUTCDATETIME()
WHERE CustomerId = @customerId
AND RowVersion = @rowVersion;

Se a atualização afetar 0 linhas, outro fluxo de trabalho já alterou o registro. O n8n pode então ler a versão mais recente e decidir se vai tentar novamente ou parar.
Eu usaria transações quando múltiplas operações SQL precisam ser bem-sucedidas juntas, mas não adicionaria locks em todos os lugares.

Obrigado pela resposta @Niffzy

Se duas execuções do n8n lerem o mesmo RowVersion e ambas tentarem atualizar o cliente, como você lidaria com a execução que recebe 0 linhas atualizadas? Você tentaria novamente automaticamente com o RowVersion mais recente, ou o n8n deveria parar e sinalizar isso como um conflito de concorrência para evitar sobrescrever acidentalmente as alterações do outro fluxo de trabalho?

Eu não sobrescreveria o registro automaticamente. Se a atualização retornar 0 linhas afetadas, geralmente significa que outra execução do n8n alterou o registro primeiro.

Eu trataria assim:
UPDATE

0 linhas afetadas?

SIM → Releia o registro mais recente

Compare as alterações

Retente apenas se for seguro

Primeiro, leia a versão mais recente:
SELECT
CustomerId,
Status,
RowVersion
FROM Customers
WHERE CustomerId = @customerId;

Em seguida, execute uma atualização protegida por versão:
UPDATE Customers
SET
Status = @status,
UpdatedAt = SYSUTCDATETIME()
WHERE CustomerId = @customerId
AND RowVersion = @rowVersion;

verifique a contagem de linhas afetadas. Se for 0, não retente a mesma atualização cegamente. Busque o registro mais recente, compare as alterações e apenas retente com a nova RowVersion se a atualização ainda for segura.

Oi Mary — a sugestão de rowversion do @Niffzy é também a escolha padrão que eu faria. Uma pequena mas importante distinção: uma transação por si só não impede uma atualização perdida, e ROWLOCK é apenas uma dica, então eu não usaria nenhuma das duas como o mecanismo principal de correção.

Eu faria a atualização retornar a nova versão:

UPDATE dbo.Customers
SET
    Status = @status,
    UpdatedAt = SYSUTCDATETIME()
OUTPUT inserted.RowVersion
WHERE CustomerId = @customerId
  AND RowVersion = @expectedRowVersion;

No n8n, eu trataria um resultado vazio / zero linhas afetadas como um conflito de concorrência:

  1. Leia a linha mais recente novamente.
  2. Reavalie se a mudança de status solicitada ainda é válida.
  3. Tente novamente apenas com o novo RowVersion, usando uma pequena contagem máxima de tentativas e jitter.
  4. Se a operação também chamar uma API externa, torne esse efeito colateral idempotente antes de tentar novamente a etapa do banco de dados.

Uma nuance extra: rowversion diz que a linha do banco de dados foi alterada depois que você a leu, mas não diz se um evento webhook recebido é mais recente em termos de negócios. Se a fonte fornece uma versão de evento monotônica ou um número de sequência, armazene isso separadamente e adicione algo como:

AND SourceVersion < @incomingSourceVersion

Eu também manteria os valores de EventId do webhook processado por trás de uma restrição única. rowversion protege contra escritores concorrentes; o ID do evento protege contra entrega duplicada. Estão relacionados, mas infelizmente sistemas distribuídos como colecionar ambos os tipos de problema :sweat_smile:

Eu reservaria UPDLOCK/HOLDLOCK para uma transação curta contendo várias instruções dependentes que genuinamente precisem de bloqueio pessimista. Para uma atualização de uma única linha, concorrência otimista geralmente é mais simples e mais gentil com a taxa de transferência.

Oi @Mary_Berry
Um UPDATE protegido por versão que não corresponde a nada ainda é uma consulta bem-sucedida, então o nó Microsoft SQL relata sucesso e as configurações “Retry On Fail” e “On Error” do n8n nunca são acionadas em caso de conflito. “Retry On Fail” é a ferramenta errada aqui de qualquer forma, já que executa novamente o mesmo nó com a mesma versão esperada desatualizada. Estruture-o como um loop explícito em vez disso: um nó IF na contagem de linhas retornadas, o ramo de conflito reconectado ao nó de releitura, e um contador no loop para limitar as tentativas.
Um detalhe para esse nó: seus Query Parameters são posicionais e referenciados como $1, $2, $3 em ordem, não como @variables nomeadas.