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.)
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.
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:
Leia a linha mais recente novamente.
Reavalie se a mudança de status solicitada ainda é válida.
Tente novamente apenas com o novo RowVersion, usando uma pequena contagem máxima de tentativas e jitter.
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
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.