Gérer les mises à jour concurrentes sans conditions de course

Bonjour à tous,
J’utilise PostgreSQL avec n8n, et j’essaie de comprendre la meilleure façon de gérer plusieurs workflows mettant à jour le même enregistrement simultanément.
Par exemple, deux exécutions de webhook pourraient essayer de mettre à jour la même ligne en même temps :
UPDATE orders
SET status = ‘processed’
WHERE order_id = 1001;
Mes préoccupations sont d’éviter les problèmes suivants :
Pertes de mises à jour
Conditions de course
Traitements en doublon
Données incohérentes
J’ai lu des articles sur le verrouillage au niveau des lignes (FOR UPDATE), le verrouillage optimiste et les transactions, mais je ne suis pas sûr quelle approche fonctionne le mieux dans un environnement n8n de production.
Pour ceux qui exécutent PostgreSQL avec des workflows à haute concurrence :
• Comptiez-vous uniquement sur les transactions, ou utilisez-vous également des verrous au niveau des lignes ?
• Quand choisiriez-vous le verrouillage optimiste plutôt que le verrouillage pessimiste ?
• Avez-vous expérimenté des blocages circulaires, et comment les avez-vous gérés ?
• Des conseils de production pour maintenir la cohérence des données sans affecter les performances ?
J’aimerais entendre ce qui a bien fonctionné pour d’autres dans des déploiements n8n réels.

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 n8n EXECUTIONS_PROCESS (par défaut : own, main) :
  • Exécution de n8n via (Docker, npm, n8n cloud, application de bureau) :
  • Système d’exploitation :

La meilleure approche dépend de la fréquence de mise à jour des mêmes enregistrements, mais pour la plupart des workflows à forte concurrence, les transactions et les verrous au niveau des lignes fonctionnent bien ensemble.

Approche recommandée

Utilisez une transaction et verrouillez la ligne avant de la mettre à jour :
BEGIN;

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE;

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

COMMIT;

Cela empêche une autre transaction de modifier la même ligne jusqu’à ce que la transaction actuelle se termine.

Pour les systèmes à fort volume
Gardez les transactions courtes
Indexez les colonnes fréquemment interrogées
Utilisez le verrouillage optimiste si les conflits de mise à jour sont rares

Salut @Keira_Becky
Pour un simple changement d’état comme dans ton exemple, ignore le verrou explicite et laisse l’UPDATE être la protection. Une seule instruction atomique qui ne touche à la ligne que si elle n’a pas encore été traitée :

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

La première exécution met à jour la ligne, la deuxième ne correspond à aucune ligne donc RETURNING revient vide, et tu fais un branchement sur « ai-je reçu une ligne » pour savoir si elle a déjà été traitée. Ça élimine les mises à jour perdues et le traitement en double en une seule instruction, pas besoin de transaction multi-étapes.

Si tu as vraiment besoin d’une vraie lecture-modification-écriture avec un verrou explicite, elle doit s’exécuter dans un seul nœud Execute Query, ou active l’option Transaction du nœud. Chaque nœud Postgres séparé ouvre sa propre connexion, donc un verrou pris dans un nœud est libéré avant que le nœud suivant ne s’exécute, et le verrou ne sert à rien.

Pour les interblocages avec des workers concurrents qui tirent un lot, utilise SKIP LOCKED pour que chaque worker récupère des lignes différentes au lieu de bloquer sur la même :

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

Ensuite, active Retry On Fail sur le nœud pour qu’une erreur de sérialisation transitoire se contente de réessayer.

Salut @Keira_Becky

Dans tout workflow n8n qui touche à PostgreSQL, encadrez toujours les actions de base de données dans une seule transaction. Dans n8n, cela se fait avec un nœud « Start Transaction », les déclarations SELECT/UPDATE requises et un nœud « Commit Transaction » (ou Rollback en cas d’erreur). Les transactions garantissent qu’une défaillance abandonne l’ensemble des modifications, ce qui évite les mises à jour partielles et maintient la cohérence des données même en cas de crash du workflow.

Le verrouillage pessimiste (SELECT … FOR UPDATE ou FOR UPDATE SKIP LOCKED) est idéal quand vous avez besoin d’un traitement exactement une fois, quand de nombreux workers se disputent quelques lignes, ou quand une logique touche plusieurs lignes qui doivent rester synchronisées. Le verrou est maintenu jusqu’à la validation de la transaction, ce qui garantit qu’aucun autre workflow ne peut lire ou modifier la ligne verrouillée, ce qui élimine les mises à jour perdues et le traitement en doublon.

Le verrouillage optimiste fonctionne mieux avec peu de contention. En ajoutant une colonne version (ou updated_at) et en effectuant la mise à jour avec une condition comme WHERE version = $oldVersion, vous permettez à plusieurs workers de tenter la mise à jour ; seul le premier réussit, et les autres détectent un conflit (zéro ligne affectée) et peuvent réessayer. Cette approche évite la surcharge des verrous et s’avère utile pour les mises à jour par lot ou les exécutions de webhook sans état, où une simple boucle de nouvelle tentative suffit.

Même avec un verrouillage prudent, des interblocages peuvent survenir quand les workflows verrouillent les lignes dans des ordres différents. Atténuez-les en acquérant toujours les verrous dans un ordre déterministe (par exemple, ORDER BY order_id ASC), en utilisant SKIP LOCKED pour le traitement de style file d’attente, et en implémentant une logique de nouvelle tentative pour l’erreur d’interblocage 40P01. La surveillance de paramètres comme log_lock_waits = on aide à détecter rapidement les incidents d’interblocage.

Pour maintenir les performances, gardez les transactions courtes, évitez les appels HTTP externes dans une transaction, et assurez-vous que les colonnes pertinentes (order_id, status, version) sont indexées. Si de nombreuses lignes ont besoin de la même modification, regroupez-les dans une seule déclaration UPDATE plutôt que de générer un workflow séparé par ligne. L’utilisation d’un pool de workers limité et de SKIP LOCKED réduit la contention des verrous et empêche les workers d’être inactifs en attendant un verrou.

Utilisez le verrouillage optimiste quand la contention est rare et que vous pouvez tolérer les nouvelles tentatives ; basculez vers le verrouillage pessimiste quand vous devez garantir un accès mono-thread ou quand plusieurs lignes interviennent dans une règle métier. Pour le traitement de style file d’attente, FOR UPDATE SKIP LOCKED combiné à une boucle courte de nouvelle tentative/back-off est le motif de production le plus courant dans n8n. Le respect de ces directives produit des workflows cohérents en données, à haut débit, sans sacrifier les performances.

Merci beaucoup @Niffzy @Anshul_Namdev @kjooleng pour l’explication détaillée

L’approche atomique UPDATE … RETURNING a du sens pour les transitions de statut simples, tandis que les transactions et les modèles de verrouillage deviennent importants quand plusieurs changements connexes doivent se produire ensemble. Cela a clarifié quand utiliser chaque approche dans les workflows de production n8n. J’apprécie les perspectives