07 sept. 2025·7 min de lecture

Verrous consultatifs PostgreSQL pour des flux de travail sûrs en cas de concurrence

Apprenez les verrous consultatifs PostgreSQL pour éviter le double-traitement dans les approbations, la facturation et les planificateurs avec des modèles pratiques, extraits SQL et vérifications simples.

Verrous consultatifs PostgreSQL pour des flux de travail sûrs en cas de concurrence

Le vrai problème : deux processus font le même travail

Le double-traitement survient lorsque le même élément est traité deux fois parce que deux acteurs différents pensent chacun être responsables. Dans les applications réelles, cela se traduit par un client facturé deux fois, une approbation appliquée en double, ou un email « facture prête » envoyé deux fois. Tout peut sembler correct en test, puis se casser sous une vraie charge.

Cela arrive généralement quand le timing devient serré et que plusieurs choses peuvent agir :

Deux workers prennent le même job en même temps. Un retry se déclenche parce qu'un appel réseau a été lent, alors que la première tentative est encore en cours. Un utilisateur double-clique sur Approuver parce que l'interface a figé une seconde. Deux planificateurs se chevauchent après un déploiement ou à cause d'une dérive d'horloge. Même un seul tap peut devenir deux requêtes si une app mobile renvoie après un timeout.

Le plus pénible, c'est que chaque acteur se comporte « raisonnablement » de son côté. Le bug est l'écart entre eux : aucun ne sait que l'autre est déjà en train de traiter le même enregistrement.

L'objectif est simple : pour un élément donné (une commande, une demande d'approbation, une facture), un seul acteur doit pouvoir effectuer le travail critique à la fois. Les autres doivent soit attendre brièvement, soit reculer et réessayer.

Les verrous consultatifs PostgreSQL peuvent aider. Ils offrent un moyen léger de dire « je traite l'élément X » en utilisant la base de données que vous utilisez déjà pour la cohérence.

Fixez les attentes toutefois. Un verrou n'est pas un système de queue complet. Il ne planifie pas de jobs pour vous, ne garantit pas l'ordre, et ne stocke pas de messages. C'est un portail de sécurité autour de la partie du workflow qui ne doit jamais s'exécuter deux fois.

Ce que sont (et ne sont pas) les verrous consultatifs PostgreSQL

Les verrous consultatifs PostgreSQL permettent de s'assurer qu'un seul worker exécute une portion de travail à la fois. Vous choisissez une clé de verrou (comme « invoice 123 »), demandez à la base de la verrouiller, faites le travail, puis relâchez le verrou.

Le mot « consultatif » est important. Postgres ne connaît pas la signification de votre clé et ne protège rien automatiquement. Il ne garde qu'un fait : cette clé est verrouillée ou elle ne l'est pas. Votre code doit s'entendre sur le format de la clé et doit prendre le verrou avant d'exécuter la partie risquée.

Il est utile de comparer les verrous consultatifs aux verrous de lignes. Les verrous de lignes (comme SELECT ... FOR UPDATE) protègent des lignes de table réelles. Ils sont parfaits quand le travail correspond clairement à une seule ligne. Les verrous consultatifs protègent une clé que vous choisissez, utile quand le workflow touche plusieurs tables, appelle des services externes, ou démarre avant qu'une ligne existe.

Les verrous consultatifs sont utiles lorsque vous avez besoin :

  • D'actions une-à-la-fois par entité (une approbation par demande, un prélèvement par facture)
  • De coordination entre plusieurs serveurs d'app sans ajouter un service de verrouillage séparé
  • De protection autour d'une étape de workflow plus large qu'une simple mise à jour de ligne

Ils ne remplacent pas d'autres outils de sécurité. Ils ne rendent pas les opérations idempotentes, n'appliquent pas les règles métiers, et n'empêcheront pas les doublons si une branche de code oublie de prendre le verrou.

On les qualifie souvent de « légers » car on peut les utiliser sans changer le schéma ni ajouter d'infrastructure. Dans de nombreux cas, on peut corriger le double-traitement en ajoutant un seul appel de verrou autour d'une section critique tout en conservant le reste du design.

Types de verrous que vous utiliserez réellement

Quand on parle de « verrous consultatifs PostgreSQL », on pense généralement à un petit ensemble de fonctions. Bien choisir change le comportement en cas d'erreurs, timeouts et retries.

Verrous de session vs verrous transactionnels

Un verrou au niveau session (pg_advisory_lock) dure tant que la connexion à la base dure. C'est pratique pour des workers longue durée, mais cela signifie également qu'un verrou peut persister si votre app plante d'une façon qui laisse une connexion du pool coincée.

Un verrou transactionnel (pg_advisory_xact_lock) est lié à la transaction courante. Quand vous committez ou faites rollback, PostgreSQL le relâche automatiquement. Pour la plupart des workflows request-response (approbations, clics de facturation, actions admin), c'est le défaut le plus sûr car il est difficile d'oublier de libérer le verrou.

Bloquant vs try-lock

Les appels bloquants attendent que le verrou soit disponible. Simple, mais cela peut rendre une requête web lente si une autre session détient le verrou.

Les appels try-lock retournent immédiatement :

  • pg_try_advisory_lock (niveau session)
  • pg_try_advisory_xact_lock (niveau transaction)

Le try-lock est souvent préférable pour les actions UI. Si le verrou est pris, vous pouvez renvoyer un message clair comme « déjà en cours » et demander à l'utilisateur de réessayer.

Partagé vs exclusif

Les verrous exclusifs sont « un à la fois ». Les verrous partagés permettent plusieurs détenteurs mais bloquent un verrou exclusif. La plupart des problèmes de double-traitement utilisent des verrous exclusifs. Les verrous partagés sont utiles quand beaucoup de lecteurs peuvent continuer, mais un écrivain rare doit exécuter seul.

Comment les verrous sont libérés

La libération dépend du type :

  • Verrous session : libérés à la déconnexion, ou explicitement avec pg_advisory_unlock
  • Verrous transactionnels : libérés automatiquement quand la transaction se termine

Choisir la bonne clé de verrou

Un verrou consultatif ne fonctionne que si chaque worker essaie de verrouiller exactement la même clé pour exactement la même unité de travail. Si un chemin de code verrouille « invoice 123 » et un autre « customer 45 », vous pouvez toujours obtenir des doublons.

Commencez par nommer la « chose » que vous voulez protéger. Soyez concret : une facture, une demande d'approbation, une exécution planifiée, ou le cycle de facturation mensuel d'un client. Ce choix décide du niveau de concurrence que vous autorisez.

Choisir un périmètre qui correspond au risque

La plupart des équipes terminent sur l'une de ces options :

  • Par enregistrement : le plus sûr pour les approbations et factures (verrou par invoice_id ou request_id)
  • Par client/compte : utile quand les actions doivent être sérialisées par client (facturation, changements de crédit)
  • Par étape de workflow : quand différentes étapes peuvent s'exécuter en parallèle, mais chaque étape doit être une-à-la-fois

Considérez le périmètre comme une décision produit, pas comme un détail de base de données. « Par enregistrement » évite les double-clicks qui entraînent un double prélèvement. « Par client » évite que deux jobs d'arrière-plan génèrent des relevés qui se chevauchent.

Choisir une stratégie de clé stable

En général vous avez deux options : deux entiers 32 bits (souvent namespace + id), ou un entier 64 bits (bigint), parfois créé en hashant un identifiant chaîne.

Les clés deux-entiers sont faciles à standardiser : choisissez un numéro de namespace fixe par workflow (par exemple approbations vs facturation), et utilisez l'ID de l'enregistrement comme second nombre.

Le hashing est pratique quand votre identifiant est un UUID, mais il faut accepter un petit risque de collision et être cohérent partout.

Quoi que vous choisissiez, documentez le format et centralisez-le. « Presque la même clé » à deux endroits réintroduit souvent des doublons.

Étape par étape : un pattern sûr pour un traitement une-à-la-fois

Déployez dans votre environnement
Déployez sur AppMaster Cloud ou votre propre environnement AWS, Azure ou Google Cloud.
Déployer

Un bon workflow avec verrous consultatifs est simple : verrouiller, vérifier, agir, enregistrer, commit. Le verrou n'est pas la règle métier en lui-même. C'est une garde qui rend la règle fiable quand deux workers touchent le même enregistrement en même temps.

Un pattern pratique :

  1. Ouvrir une transaction quand le résultat doit être atomique.
  2. Acquérir le verrou pour l'unité de travail spécifique. Préférez un verrou transactionnel (pg_advisory_xact_lock) pour qu'il se relâche automatiquement.
  3. Re-vérifier l'état dans la base. Ne partez pas du principe que vous êtes le premier. Confirmez que l'enregistrement est toujours éligible.
  4. Faites le travail et écrivez un marqueur durable « fait » dans la base (mise à jour de statut, écriture en ledger, ligne d'audit).
  5. Committez et laissez le verrou partir. Si vous avez utilisé un verrou session, déverrouillez avant de rendre la connexion au pool.

Exemple : deux serveurs reçoivent « Approuver la facture #123 » dans la même seconde. Les deux démarrent, mais un seul obtient le verrou pour 123. Le gagnant vérifie que la facture #123 est toujours pending, la marque approved, écrit l'audit/le paiement, et commit. Le deuxième serveur n'obtient pas le verrou (try-lock) ou attend puis re-vérifie et sort sans créer de doublon. Dans les deux cas, on évite le double-traitement tout en gardant l'UI réactive.

Où les verrous consultatifs s'intègrent : approbations, facturation, planificateurs

Les verrous consultatifs conviennent le mieux quand la règle est simple : pour une chose précise, un seul processus peut faire le travail « gagnant » à la fois. Vous conservez votre base et votre code, mais ajoutez une petite barrière qui rend les conditions de course beaucoup moins probables.

Approbations

Les approbations sont des pièges classiques de concurrence. Deux réviseurs (ou la même personne en double-cliquant) peuvent appuyer Approuver en quelques millisecondes. Avec un verrou basé sur l'ID de la demande, une seule transaction effectue le changement d'état. Les autres apprennent rapidement le résultat et peuvent afficher un message clair comme « déjà approuvé » ou « déjà rejeté ».

C'est courant dans les portails clients et panels admin où beaucoup de personnes regardent la même file.

Facturation

La facturation nécessite habituellement une règle plus stricte : une tentative de paiement par facture, même en cas de retries. Un timeout réseau peut pousser un utilisateur à cliquer Pay une nouvelle fois, ou un retry d'arrière-plan peut s'exécuter pendant que la première tentative est encore en vol.

Un verrou basé sur l'ID de la facture garantit qu'une seule voie parle au prestataire de paiement à la fois. La seconde tentative peut retourner « paiement en cours » ou lire le statut de paiement le plus récent. Cela évite le travail dupliqué et réduit le risque de double prélèvement.

Planificateurs et workers en arrière-plan

Dans des architectures multi-instance, les planificateurs peuvent exécuter la même fenêtre en parallèle par erreur. Un verrou sur le nom du job plus la fenêtre temporelle (par exemple « daily-settlement:2026-01-29 ») garantit qu'une seule instance l'exécute.

La même approche fonctionne pour les workers qui extraient des éléments d'une table : verrouillez sur l'ID de l'item pour qu'un seul worker le traite.

Des clés communes : un ID de demande, un ID de facture, un nom de job plus fenêtre temporelle, un ID client pour « une exportation à la fois », ou une clé unique d'idempotence pour les retries.

Un exemple réaliste : empêcher la double-approbation dans un portail

Évitez les doubles approbations dans votre portail
Créez un flux d'approbation dans AppMaster et protégez l'étape de finalisation avec un verrou Postgres.
Essayer AppMaster

Imaginez une demande d'approbation : un bon de commande attend, et deux managers cliquent Approuver dans la même seconde. Sans protection, les deux requêtes lisent « pending » et écrivent « approved », générant des entrées d'audit en double, des notifications en double, ou du travail descendant déclenché deux fois.

Les verrous consultatifs PostgreSQL offrent un moyen simple de rendre cette action une-à-la-fois par approval.

Le flux

Quand l'API reçoit une action d'approbation, elle prend d'abord un verrou basé sur l'ID d'approbation (ainsi différentes approbations peuvent encore être traitées en parallèle).

Un pattern commun : verrouiller sur approval_id, lire le statut courant, mettre à jour le statut, puis écrire un enregistrement d'audit, le tout dans une même transaction.

BEGIN;

-- One-at-a-time per approval_id
SELECT pg_try_advisory_xact_lock($1) AS got_lock;  -- $1 = approval_id

-- If got_lock = false, return "someone else is approving, try again".

SELECT status FROM approvals WHERE id = $1 FOR UPDATE;

-- If status != 'pending', return "already processed".

UPDATE approvals
SET status = 'approved', approved_by = $2, approved_at = now()
WHERE id = $1;

INSERT INTO approval_audit(approval_id, actor_id, action, created_at)
VALUES ($1, $2, 'approved', now());

COMMIT;

Ce que vit le deuxième clic

La seconde requête ne peut pas obtenir le verrou (elle retourne rapidement « Déjà en cours ») ou elle obtient le verrou après que le premier ait fini, puis constate que le statut est déjà approved et sort sans rien modifier. Dans les deux cas, vous évitez le double-traitement tout en gardant l'interface réactive.

Pour le debugging, consignez suffisamment d'informations pour tracer chaque tentative : id de requête, id d'approbation et clé de verrou calculée, id de l'acteur, résultat (lock_busy, already_approved, approved_ok) et durées.

Gérer attente, timeouts et retries sans geler l'application

Rendre les retries de facturation sûrs
Ajoutez un traitement factures une-à-la-fois avant d'appeler Stripe ou un autre prestataire de paiements.
Commencer

Attendre un verrou semble anodin jusqu'à ce que cela devienne un bouton qui tourne, un worker bloqué, ou un backlog qui ne décolle jamais. Quand vous n'obtenez pas le verrou, échouez vite là où un humain attend et attendez seulement là où l'attente est sûre.

Pour les actions utilisateur : try-lock et réponse claire

Si quelqu'un clique Approuver ou Payer, ne bloquez pas sa requête pendant des secondes. Utilisez try-lock afin que l'app puisse répondre immédiatement.

Une approche pratique : essayer de prendre le verrou et, en cas d'échec, retourner une réponse claire « occupé, réessayez » (ou rafraîchir l'état de l'élément). Cela réduit les timeouts et décourage les clics répétés.

Gardez la section verrouillée courte : validez l'état, appliquez le changement, commit.

Pour les jobs background : bloquer peut aller, mais plafonnez

Pour les planificateurs et workers, bloquer peut être acceptable car aucun humain n'attend. Mais vous devez quand même imposer des limites, sinon un job lent peut paralyser une flotte entière.

Utilisez des timeouts pour qu'un worker puisse abandonner et passer à autre chose :

SET lock_timeout = '2s';
SET statement_timeout = '30s';
SELECT pg_advisory_lock(123456);

Définissez aussi une durée maximale attendue pour le job lui-même. Si la facturation se termine normalement en moins de 10 secondes, considérez 2 minutes comme un incident. Suivez l'heure de début, l'id du job et la durée des verrous. Si votre runner supporte l'annulation, annulez les tâches qui dépassent la limite afin que la session se termine et que le verrou soit libéré.

Planifiez les retries volontairement. Quand un verrou n'est pas acquis, décidez : replanifier bientôt avec backoff (et un peu d'aléa), sauter le travail best-effort pour ce cycle, ou marquer l'élément comme en contention si les échecs se répètent et nécessitent attention.

Erreurs courantes qui causent verrous bloqués ou duplications

La surprise la plus fréquente est un verrou session qui n'est jamais libéré. Les pools de connexion gardent les connexions ouvertes, donc une session peut survivre à une requête. Si vous prenez un verrou session et oubliez de le déverrouiller, il peut rester tenu jusqu'à ce que la connexion soit recyclée. D'autres workers attendront (ou échoueront) et il peut être difficile de comprendre pourquoi.

Une autre source de duplications est de prendre le verrou sans re-vérifier l'état. Un verrou n'assure qu'un seul worker exécute la section critique à la fois. Il ne garantit pas que l'enregistrement est encore éligible. Vérifiez toujours l'état à l'intérieur de la même transaction (par exemple confirmez pending avant de passer à approved).

Les clés de verrou trompent aussi les équipes. Si un service verrouille sur order_id et qu'un autre calcule une clé différente pour la même ressource réelle, vous avez maintenant deux verrous. Les deux chemins peuvent s'exécuter en parallèle, créant une fausse impression de sécurité.

Les verrous tenus longtemps sont généralement auto-infligés. Si vous effectuez des appels réseau lents pendant que vous maintenez le verrou (prestataire de paiement, envoi d'email/SMS, webhooks), une petite garde devient un goulot d'étranglement. Gardez la section verrouillée concentrée sur des opérations rapides en base : valider l'état, écrire le nouvel état, enregistrer ce qui doit arriver ensuite. Puis déclenchez les effets secondaires après le commit.

Enfin, les verrous consultatifs ne remplacent pas l'idempotence ni les contraintes de base. Traitez-les comme un feu de circulation, pas comme une preuve. Utilisez des contraintes uniques quand c'est approprié, et des clés d'idempotence pour les appels externes.

Checklist rapide avant de déployer

Corrigez d'abord une action critique
Choisissez « facturer une invoice » ou « approuver une demande » et entourez-le d'un verrou transactionnel.
Commencer

Traitez les verrous consultatifs comme un petit contrat : toute l'équipe doit savoir ce que le verrou protège et ce qui est autorisé pendant qu'il est détenu.

Une checklist qui attrape la plupart des problèmes :

  • Une clé de verrou claire par ressource, documentée et réutilisée partout
  • Acquérir le verrou avant toute action irréversible (paiements, emails, appels API externes)
  • Re-vérifier l'état après la prise du verrou et avant d'écrire des changements
  • Garder la section verrouillée courte et mesurable (journaliser le temps d'attente et d'exécution du verrou)
  • Décider ce que signifie « verrou occupé » pour chaque chemin (message UI, retry avec backoff, sauter)

Prochaines étapes : appliquer le pattern et le maintenir

Choisissez un endroit où les duplications font le plus mal et commencez par là. Les premières cibles : actions coûteuses ou qui modifient l'état de façon permanente, comme « débiter une facture » ou « approuver une demande ». Entourez uniquement cette section critique d'un verrou consultatif, puis étendez aux étapes proches une fois que vous faites confiance au comportement.

Ajoutez de l'observabilité tôt. Consignez quand un worker ne peut pas obtenir un verrou, et combien de temps durent les traitements verrouillés. Si les temps d'attente augmentent, cela signifie généralement que la section critique est trop grande ou qu'une requête lente s'y cache.

Les verrous fonctionnent mieux en complément de la sécurité des données, pas à la place. Gardez des champs d'état clairs (pending, processing, done, failed) et soutenez-les par des contraintes quand c'est possible. Si un retry survient au pire moment, une contrainte unique ou une clé d'idempotence peut être la seconde ligne de défense.

Si vous construisez des workflows dans AppMaster (appmaster.io), vous pouvez appliquer le même pattern en gardant le changement d'état critique dans une seule transaction et en ajoutant une petite étape SQL pour prendre un verrou consultatif transactionnel avant l'étape de « finalisation ».

Les verrous consultatifs conviennent jusqu'à ce que vous ayez réellement besoin des fonctionnalités d'une queue (priorités, jobs différés, dead-letter), que la contention soit lourde et nécessite un parallélisme plus fin, que vous deviez coordonner plusieurs bases sans Postgres partagé, ou que vous ayez besoin de règles d'isolation plus strictes. L'objectif est la fiabilité banale : gardez le pattern petit, cohérent, visible dans les logs et soutenu par des contraintes.

FAQ

Quand dois-je utiliser les verrous consultatifs PostgreSQL plutôt que de me fier à la logique de l'application ?

Utilisez un verrou consultatif lorsque vous avez besoin que « un seul acteur à la fois » exécute une unité de travail précise, comme approuver une demande, débiter une facture ou exécuter une fenêtre planifiée. C'est particulièrement utile quand plusieurs instances de l'application peuvent toucher la même entité et que vous ne voulez pas ajouter un service de verrouillage séparé.

En quoi les verrous consultatifs diffèrent-ils des verrous de type SELECT ... FOR UPDATE ?

Les verrous de ligne protègent des lignes réelles que vous sélectionnez et sont parfaits quand l'opération correspond clairement à une mise à jour d'une seule ligne. Les verrous consultatifs protègent une clé que vous définissez, ils fonctionnent donc même lorsque le workflow manipule plusieurs tables, appelle des services externes ou démarre avant que la ligne finale n'existe.

Dois-je utiliser des verrous transactionnels ou des verrous session ?

Par défaut, privilégiez pg_advisory_xact_lock (verrou transactionnel) pour les actions request/response car il est relâché automatiquement au commit ou au rollback. N'utilisez pg_advisory_lock (verrou session) que si vous avez réellement besoin que le verrou survive à la transaction et que vous êtes sûr d'appeler pg_advisory_unlock avant de rendre la connexion au pool.

Est-il préférable d'attendre un verrou ou d'utiliser un try-lock ?

Pour les actions pilotées par l'interface, préférez le try-lock (pg_try_advisory_xact_lock) afin que la requête échoue vite et retourne une réponse claire « déjà en cours ». Pour les workers en arrière-plan, un verrou bloquant peut convenir, mais imposez un lock_timeout pour qu'une tâche coincée ne bloque pas toute la flotte.

Sur quoi dois-je verrouiller : id d'enregistrement, id client ou autre chose ?

Verrouillez la plus petite unité qui ne doit pas être exécutée deux fois, en général « une facture » ou « une demande d'approbation ». Verrouiller trop largement (par exemple par client) réduit le débit ; verrouiller trop finement peut laisser des duplications.

Comment choisir une clé de verrou pour que tous les services utilisent exactement la même ?

Choisissez un format de clé stable et utilisez-le partout où la même action critique peut être exécutée. Une approche courante : deux entiers — un namespace fixe pour le workflow et l'ID de l'entité — afin que différents workflows ne se bloquent pas entre eux tout en restant coordonnés.

Les verrous consultatifs remplacent-ils les contrôles d'idempotence ou les contraintes d'unicité ?

Non. Un verrou empêche l'exécution concurrente ; il ne prouve pas que l'opération est sûre à répéter. Vous devez toujours retraiter l'état à l'intérieur de la transaction (par exemple vérifier que l'élément est encore pending) et vous appuyer sur des contraintes uniques ou des clés d'idempotence là où c'est pertinent.

Que dois-je faire à l'intérieur de la section verrouillée pour éviter de tout ralentir ?

Gardez la section verrouillée courte et centrée sur la base de données : acquérir le verrou, re-vérifier l'éligibilité, écrire le nouvel état, committer. Effectuez les effets latéraux lents (paiements, emails, webhooks) après le commit ou via une approche outbox pour ne pas tenir le verrou pendant des appels réseau.

Pourquoi les verrous consultatifs semblent-ils parfois « coincés » alors que la requête est terminée ?

La cause la plus fréquente est un verrou session maintenu par une connexion persistante du pool qui n'a jamais été déverrouillée suite à un bug. Préférez les verrous transactionnels ; si vous devez utiliser des verrous session, assurez-vous que pg_advisory_unlock est appelé avant de rendre la connexion au pool.

Que dois-je journaliser ou surveiller pour savoir si les verrous consultatifs fonctionnent ?

Consignez l'ID de l'entité et la clé de verrou calculée, indiquez si le verrou a été acquis, combien de temps cela a pris pour l'obtenir et la durée de la transaction. Journalisez aussi les résultats (lock_busy, already_processed, processed_ok) afin de distinguer la contention d'erreurs réelles.

Facile à démarrer
Créer quelque chose d'incroyable

Expérimentez avec AppMaster avec un plan gratuit.
Lorsque vous serez prêt, vous pourrez choisir l'abonnement approprié.

Démarrer