Le verrouillage optimiste : détecter le conflit au bon moment
Détecter qu’une donnée a changé depuis sa lecture, refuser proprement l’écriture concurrente et éviter d’écraser silencieusement le travail d’une autre personne.
Où en êtes-vous avec cette notion ?
Une indication personnelle, enregistrée uniquement dans ce navigateur.
À lire ensuite
Le format café
L’essentiel à comprendre, le temps d’un café.
Envie de creuser ? Un cours plus complet vous attend juste après, à déplier sans quitter cette page.
Ce que vous allez comprendre
Comment éviter qu’une personne écrase sans le savoir la modification d’une autre, sans bloquer qui que ce soit pendant qu’il remplit un formulaire.
Deux personnes, une fiche
Alice et Bob ouvrent la même fiche client au même moment.
Alice ouvre : version 7 Bob ouvre : version 7
Alice change le téléphone
Alice enregistre → version 8
Bob change la ville
Bob enregistre tout son formulaire
→ le téléphone revient à l’ancienne valeur
Bob n’a pourtant touché qu’au champ « ville ». Son formulaire renvoyait aussi l’ancien téléphone. La modification d’Alice a disparu sans le moindre message : c’est une perte de modification silencieuse, souvent appelée lost update.
Une colonne version, une condition dans l’UPDATE
Ajoutez une colonne version à la table. Le client la reçoit avec les données et la renvoie au moment d’enregistrer. Depuis psql, sur une table customers qui possède déjà cette colonne :
UPDATE customers
SET city = 'Nîmes', version = version + 1
WHERE id = 42 AND version = 7;
Si la fiche est encore en version 7, la ligne est modifiée et la version passe à 8. Si Alice est passée avant, la base contient déjà la version 8 : zéro ligne modifiée. Votre application lit ce compteur et répond 409 Conflict au lieu d’écraser.
C’est le verrouillage optimiste : on suppose que les conflits sont rares, on ne bloque personne, on détecte au moment de l’écriture.
Le contraste avec le verrouillage pessimiste
SELECT … FOR UPDATE verrouille la ligne et fait attendre les autres. C’est adapté à une opération courte et sensible. Ce n’est pas envisageable pendant les douze minutes où quelqu’un remplit un formulaire : la transaction resterait ouverte tout ce temps.
Une variante remplace version par updated_at, avec la même condition dans l’UPDATE. Le principe est identique, mais un compteur explicite reste plus simple à raisonner.
Le même mécanisme existe en HTTP
Le serveur envoie un ETag avec la ressource, le client le renvoie dans If-Match avec sa modification, et le serveur n’applique le changement que si la ressource est toujours dans cette version.
Le piège
Vérifier la version dans le code, puis lancer l’UPDATE, recrée exactement la race condition vue au cours précédent : une autre requête peut passer entre les deux. La condition doit vivre dans l’UPDATE.
Fiche mémo
- Lecture : renvoyer la version au client avec les données.
- Écriture :
WHERE id = ? AND version = ?, etversion = version + 1. - Une ligne modifiée : succès. Zéro ligne : conflit, donc
409. - Jamais de vérification séparée de l’écriture.
L’idée à emporter : on ne bloque personne à l’avance ; on détecte le conflit à l’instant précis où il ferait perdre des données.
Et si on allait plus loin ?
Le café vous a donné les repères. Prenez maintenant le temps de comprendre les mécanismes et de pratiquer, si vous le souhaitez.
Aller plus loinDu conflit détecté à la reprise du travail par l’utilisateurDévelopper le coursReplier le cours
Ce que vous saurez faire
Mettre en place une colonne de version, transformer « zéro ligne modifiée » en une réponse HTTP utile, et concevoir ce qui se passe après la détection du conflit. Travaillez sur une base d’exercice vide.
1. La version vit dans la ligne, la condition dans l’UPDATE
Contexte : psql, base d’exercice vide. L’atelier crée lui-même la table.
CREATE TABLE customers (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
phone text,
city text,
version integer NOT NULL DEFAULT 1
);
INSERT INTO customers (name, phone, city)
VALUES ('Acme', '04 00 00 00 00', 'Montpellier');
UPDATE customers SET phone = '04 11 11 11 11', version = version + 1
WHERE id = 1 AND version = 1;
UPDATE customers SET city = 'Nîmes', version = version + 1
WHERE id = 1 AND version = 1;
Résultat attendu : UPDATE 1, puis UPDATE 0. Le second ordre porte une version périmée : il ne modifie rien et n’écrase rien. C’est le seul élément du dispositif qui garantit quelque chose, parce que la condition et l’incrémentation appartiennent à la même instruction et sont donc évaluées ensemble par la base.
Référence : UPDATE.
Ce dépôt applique exactement ce schéma. Le changement d’état d’une ressource éditoriale, dans contentRepository.transition (src/lib/repository.ts), envoie une mise à jour conditionnée sur l’identifiant et sur la version attendue, puis signale un conflit lorsque aucune ligne n’a été modifiée. C’est un exemple ponctuel, pas une description de l’architecture du projet.
2. Zéro ligne modifiée est une réponse, pas une panne
Côté serveur, le nombre de lignes modifiées décide du code HTTP.
// Contexte : Node.js, dans un gestionnaire de route ; la requête SQL est celle du point 1.
const lignesModifiees = await mettreAJourClient(id, champs, versionRecue);
if (lignesModifiees === 0) {
return conflit409(await lireClient(id));
}
Résultat attendu : un 200 avec la nouvelle version en cas de succès, un 409 Conflict sinon. Ne renvoyez pas 404, car la fiche existe : c’est sa version qui a changé. Ne renvoyez pas 500 non plus, car rien n’est cassé : la protection a fait son travail.
Le même raisonnement existe au niveau du protocole. Le serveur expose la version dans un en-tête ETag, le client la renvoie dans If-Match, et le serveur refuse la modification si la ressource a changé, généralement avec 412 Precondition Failed. La condition est simplement déplacée de la base vers HTTP.
Référence : l’en-tête If-Match.
3. Détecter le conflit ne le résout pas
C’est la limite la plus souvent oubliée. Un message « cette fiche a été modifiée depuis votre dernière lecture » qui ramène l’utilisateur à un formulaire vide lui fait perdre son travail. Il recommencera, et cette fois il écrasera volontairement.
Une réponse 409 utile transporte l’état actuel de la ressource et sa nouvelle version. L’interface peut alors :
- montrer quels champs ont changé depuis la lecture, et par qui si l’information existe ;
- conserver la saisie en cours pour qu’elle n’ait pas à être retapée ;
- proposer une action explicite : reprendre la valeur distante, garder la sienne, ou arbitrer champ par champ.
Lorsque les modifications portent sur des champs différents, le téléphone d’un côté et la ville de l’autre, une fusion automatique est souvent possible et évite d’interrompre les deux personnes. Cela suppose d’envoyer uniquement les champs réellement modifiés, et non le formulaire entier.
Pièges et limites
- Vérifier la version avant l’
UPDATE. UnSELECTde contrôle suivi d’une écriture séparée laisse un intervalle pendant lequel une autre requête peut passer. C’est exactement la race condition que le mécanisme prétend éviter. - Faire confiance à
updated_at. La variante fonctionne, mais la granularité de l’horloge la fragilise : deux écritures tombant dans la même unité de temps produisent la même valeur, et la condition laisse alors passer une modification qu’elle aurait dû refuser. Une colonneversionentière n’a pas ce défaut. - Oublier d’incrémenter. Si l’
UPDATEfiltre sur la version sans la faire avancer, toutes les écritures réussissent et la protection devient purement décorative. - Traiter le conflit comme une erreur serveur. Déclencher une alerte à chaque
409sature la supervision : un conflit est un événement métier attendu dans une application collaborative. - Confondre avec le verrouillage pessimiste.
SELECT … FOR UPDATEfait attendre les autres et suppose une transaction courte ; il ne convient pas à un formulaire ouvert plusieurs minutes.
À vous de jouer
Reprenez la table customers de l’atelier : la fiche 1 est maintenant en version 2. Une requête arrive avec version = 1 et veut écrire la ville. Écrivez l’UPDATE, indiquez ce que PostgreSQL renvoie, puis décrivez la réponse HTTP que le serveur doit produire, contenu compris.
Correction commentée
-- Contexte : psql, suite de l’atelier
UPDATE customers
SET city = 'Sète', version = version + 1
WHERE id = 1 AND version = 1
RETURNING id, version;
PostgreSQL renvoie UPDATE 0 et aucune ligne : la condition version = 1 ne correspond plus, puisque la fiche est en version 2. Rien n’a été écrasé, et c’est précisément le résultat recherché.
Le serveur répond 409 Conflict. Le corps ne se limite pas à un message : il transporte la fiche telle qu’elle est maintenant, avec sa version 2, pour que l’interface puisse montrer ce qui a changé et proposer de reprendre la saisie sans la perdre.
Pourquoi cette réponse est la bonne. La condition et l’incrémentation appartiennent à la même instruction, donc aucun intervalle n’est exploitable par une requête concurrente. Le nombre de lignes modifiées est la seule information fiable sur l’issue de l’opération, bien plus que l’absence d’erreur SQL. Et RETURNING permet, en cas de succès, de renvoyer immédiatement la nouvelle version au client sans relire la ligne, ce qui évite un aller-retour et surtout une seconde source de vérité. Le conflit est ainsi traité comme une situation normale du travail à plusieurs, et non comme un incident.
Vous pouvez aussi vous arrêter ici. L’approfondissement est facultatif.
Les repères Cours Café
Pour aller à la source
Documentation de référence. Vérification éditoriale encore à effectuer.
Gardez une trace de cette idée.
Favoris, notes et progression seront disponibles après connexion du stockage distant.