Salut les gars, j’utilise n8n avec Microsoft SQL Server, et plusieurs exécutions de workflow peuvent mettre à jour le même enregistrement client presque simultanément.
Je m’inquiète des conditions de concurrence et des mises à jour perdues lorsque deux exécutions lisent le même enregistrement et essaient ensuite de le mettre à jour.
UPDATE Customers
SET
Status = @status,
UpdatedAt = GETUTCDATE()
WHERE CustomerId = @customerId;
Recommanderiez-vous des transactions, UPDLOCK/ROWLOCK, une concurrence optimiste en utilisant une colonne rowversion, ou une combinaison ? De plus, comment structureriez-vous le workflow n8n afin qu’une mise à jour échouée ou concurrente puisse être relancée en toute sécurité sans écraser les données plus récentes ?
Décrivez le problème/l’erreur/la question
Quel est le message d’erreur (le cas échéant) ?
Veuillez partager votre workflow
(Sélectionnez les nœuds sur votre canevas et utilisez les raccourcis clavier CMD+C/CTRL+C et CMD+V/CTRL+V pour copier et coller le workflow.)
Partagez la sortie renvoyée par le dernier nœud
Informations sur votre configuration n8n
- Version n8n :
- Base de données (par défaut : SQLite) :
- Paramètre EXECUTIONS_PROCESS de n8n (par défaut : own, main) :
- Exécution de n8n via (Docker, npm, n8n cloud, application de bureau) :
- Système d’exploitation :
Bonjour @Mary_Berry
Utilisez la concurrence optimiste avec la colonne rowversion de SQL Server, en particulier lorsque plusieurs exécutions n8n peuvent mettre à jour le même enregistrement.
Ajoutez une colonne rowversion :
ALTER TABLE Customers
ADD RowVersion ROWVERSION;
Lorsque n8n lit le client, conservez la rowversion actuelle. Ensuite, mettez à jour l’enregistrement uniquement si cette version est toujours inchangée : UPDATE Customers
SET
Status = @status,
UpdatedAt = SYSUTCDATETIME()
WHERE CustomerId = @customerId
AND RowVersion = @rowVersion;
Si la mise à jour affecte 0 lignes, un autre flux de travail a déjà modifié l’enregistrement. n8n peut alors relire la dernière version et décider de réessayer ou d’arrêter.
J’utiliserais les transactions lorsque plusieurs opérations SQL doivent réussir ensemble, mais je n’ajouterais pas de verrous partout.
Merci pour la réponse @Niffzy
Si deux exécutions n8n lisent le même RowVersion et tentent toutes les deux de mettre à jour le client, comment géreriez-vous l’exécution qui obtient 0 lignes mises à jour ? Relanceriez-vous automatiquement avec le dernier RowVersion, ou n8n devrait-il s’arrêter et le signaler comme un conflit de concurrence pour éviter de remplacer accidentellement les modifications de l’autre workflow ?
Je ne surchargerais pas automatiquement l’enregistrement. Si la mise à jour retourne 0 lignes affectées, cela signifie généralement qu’une autre exécution n’8n a modifié l’enregistrement en premier.
Je le gérerais comme ceci :
MISE À JOUR
↓
0 lignes affectées ?
↓
OUI → Relire l’enregistrement le plus récent
↓
Comparer les modifications
↓
Réessayer uniquement si c’est sûr
D’abord, lisez la version la plus récente :
SELECT
CustomerId,
Status,
RowVersion
FROM Customers
WHERE CustomerId = @customerId;
Ensuite, effectuez une mise à jour protégée par version :
UPDATE Customers
SET
Status = @status,
UpdatedAt = SYSUTCDATETIME()
WHERE CustomerId = @customerId
AND RowVersion = @rowVersion;
vérifiez le nombre de lignes affectées. S’il est 0, ne réessayez pas la même mise à jour aveuglément. Récupérez l’enregistrement le plus récent, comparez les modifications, et réessayez uniquement avec le nouveau RowVersion si la mise à jour est toujours sûre.
Salut Mary — la suggestion de rowversion de @Niffzy est aussi le choix par défaut que je ferais. Une petite distinction, mais importante : une transaction en elle-même ne prévient pas une perte de mise à jour, et ROWLOCK n’est qu’un indice, donc je n’utiliserais ni l’un ni l’autre comme mécanisme de correction principal.
Je ferais en sorte que la mise à jour retourne la nouvelle version :
UPDATE dbo.Customers
SET
Status = @status,
UpdatedAt = SYSUTCDATETIME()
OUTPUT inserted.RowVersion
WHERE CustomerId = @customerId
AND RowVersion = @expectedRowVersion;
Dans n8n, je traiterais un résultat vide / zéro ligne affectée comme un conflit de concurrence :
- Relire la dernière ligne.
- Réévaluer si le changement de statut demandé est toujours valide.
- Réessayer uniquement avec la nouvelle
RowVersion, en utilisant un petit nombre de tentatives maximales et une variation aléatoire.
- Si l’opération appelle également une API externe, rendez cet effet secondaire idempotent avant de réessayer l’étape de base de données.
Une nuance supplémentaire : rowversion vous indique que la ligne de la base de données a changé après l’avoir lue, mais ne vous dit pas si un événement webhook entrant est plus récent en termes métier. Si la source fournit une version d’événement monotone ou un numéro de séquence, stockez-le séparément et ajoutez quelque chose comme :
AND SourceVersion < @incomingSourceVersion
Je conserverais également les valeurs EventId des webhooks traités derrière une contrainte d’unicité. rowversion protège contre les écrivains concurrents ; l’ID d’événement protège contre la livraison en double. Ils sont liés, mais malheureusement les systèmes distribués aiment collectionner les deux types de problèmes 
Je réserverais UPDLOCK/HOLDLOCK à une courte transaction contenant plusieurs instructions dépendantes qui ont réellement besoin d’un verrouillage pessimiste. Pour une mise à jour d’une seule ligne, l’optimisme concurrent est généralement plus simple et meilleur pour le débit.
Bonjour @Mary_Berry
Une UPDATE gardée par version qui ne correspond à rien est toujours une requête réussie, donc le nœud Microsoft SQL signale le succès et les paramètres « Retry On Fail » et « On Error » de n8n ne s’engagent jamais en cas de conflit. « Retry On Fail » est de toute façon le mauvais outil ici, puisqu’il réexécute le même nœud avec la même version attendue obsolète. Structurez-le plutôt comme une boucle explicite : un nœud IF sur le nombre de lignes renvoyées, la branche de conflit reconnectée au nœud de relecture, et un compteur dans la boucle pour limiter les tentatives.
Un détail pour ce nœud : ses Query Parameters sont positionnels et référencés comme $1, $2, $3 dans l’ordre, et non comme des @variables nommées.