5204 sujets

Le Bar du forum

Modérateur
Bonjour à tous,

Je souhaitais partager avec la communauté un problème récurrent que je vois passer aussi bien chez les débutants que chez les développeurs confirmés. Il concerne la gestion des ressources partagées, typiquement le stock dans une boutique en ligne, mais aussi les places de spectacle, les réservations, ou tout système où une quantité limitée est disponible.

Le code qui semble parfaitement logique
Prenons un exemple simplifié, dans un pseudo-code compréhensible par tous :


fonction passerCommande(idProduit, quantite) {
    // 1. On récupère le produit
    produit = recupererProduit(idProduit)
    
    // 2. On vérifie le stock
    si (produit.stock < quantite) {
        retourner ERREUR "Stock insuffisant"
    }
    
    // 3. On crée la commande
    commande = creerCommande(idProduit, quantite)
    
    // 4. On met à jour le stock
    produit.stock = produit.stock - quantite
    sauvegarder(produit)
    
    retourner commande
}


Ce code est clair, logique, et fonctionne parfaitement quand on le teste seul.

Le problème : la race condition

Imaginez maintenant un produit très demandé. Il ne reste que 1 seul exemplaire en stock.

- Utilisateur Alice (10h00m00.001s) : La fonction est appelée. Le stock lu est 1. La vérification stock < quantité est fausse (1 < 1), donc on passe à la suite.
- Utilisateur Bob (10h00m00.050s) : La fonction est appelée 50 millisecondes plus tard, alors qu'Alice n'a pas encore fini de sauvegarder. Bob lit lui aussi stock = 1. Sa vérification passe également.

Résultat final :
- Alice obtient sa commande. Le stock passe à 0.
- Bob obtient aussi sa commande. Le stock passe à -1.

Vous venez de vendre deux fois le même produit unique.

Pourquoi ce bug est-il si courant ?


Parce qu'il est silencieux. En développement local, avec un seul utilisateur, il est impossible à reproduire. Il ne se manifeste qu'en production, sous charge, souvent au pire moment (lancement d'un produit, soldes, Black Friday).

Techniquement, c'est un problème de concurrence (race condition) et de pattern « Check-then-Act » (vérifier puis agir). Entre la vérification et l'action, l'état du système a changé.

La solution : atomicité et verrouillage

Pour résoudre ce problème de manière universelle, quel que soit le langage ou le framework utilisé, deux mécanismes doivent être combinés :

1. La transaction atomique

Une transaction garantit que toutes les opérations d'un bloc réussissent ensemble ou échouent ensemble. Si une erreur survient à n'importe quelle étape, tout est annulé (ROLLBACK).


DEBUT TRANSACTION
    // Toutes les opérations ici
FIN TRANSACTION (COMMIT si tout va bien, ROLLBACK sinon)


2. Le verrouillage pessimiste

Avant même de vérifier le stock, il faut verrouiller la ligne concernée dans la base de données. Cela empêche toute autre connexion de lire ou modifier cette ligne tant que la transaction n'est pas terminée.

En SQL, cela se traduit par SELECT ... FOR UPDATE.

DEBUT TRANSACTION
    // Verrouiller la ligne du produit
    produit = SELECT * FROM produit WHERE id = idProduit FOR UPDATE
    
    // Maintenant, personne d'autre ne peut toucher à cette ligne
    SI produit.stock < quantite ALORS
        ROLLBACK
        RETOURNER ERREUR
    FIN SI
    
    // Création de la commande
    INSERT INTO commande ...
    
    // Mise à jour atomique du stock (calcul fait par le SGBD)
    UPDATE produit SET stock = stock - quantite WHERE id = idProduit
    
COMMIT


3. La mise à jour atomique

Plutôt que de faire stock = stock - quantite dans le code applicatif, il est préférable de laisser le SGBD faire le calcul :


UPDATE produit SET stock = stock - 1 WHERE id = 42;


Cela évite de relire une valeur potentiellement déjà modifiée.

Ce qu'il faut retenir

Dès que votre application manipule une ressource partagée et limitée (stock, places, crédits, solde), vous devez :

1. Identifier le bloc critique qui lit puis écrit.
2. L'encapsuler dans une transaction.
3. Utiliser un verrou pessimiste sur la ressource convoitée.
4. Privilégier les mises à jour atomiques en base de données.

Ce n'est pas une question de langage ou de framework. Que vous codiez en PHP, Python, Java, Ruby ou JavaScript, le problème et la solution sont les mêmes. Seule la syntaxe change.

J'ai rédigé un article détaillé sur mon blog qui applique ces principes à un cas concret avec Django et cas Django REST Framework, mais les concepts restent universels.

Note : J'ai hésité à publier ce sujet ici, sachant qu'Alsacréations est historiquement plus orienté frontend. Mais les problématiques de concurrence et d'intégrité des données touchent aussi les développeurs full-stack, et je pense que le forum a toute sa place pour ce type de partage backend. N'hésitez pas à me dire si ce format « article » est le bienvenu ou s'il aurait été plus pertinent ailleurs.
Modifié par Niuxe (19 Apr 2026 - 23:46)
Bonjour Niuxe,

C'est là que se dévoile la supériorité d'une approche db-first par rapport à une logique applicative.

En dernier recours pensez â mettre une contrainte CHECK stock >= 0 comme rempart structurel ultime.
Hello et merci Niuxe !

Je trouve ce sujet très pertinent. Gérer les accès concurrents c'est crucial, surtout dans les situations que tu as mentionnées en exemple.

En revanche, dans les cas où les lectures sont beaucoup plus fréquentes que les écritures le verrou pessimiste devient problématique pour les performances. Car pendant que la ligne à modifier est verrouillée, personne d'autre ne peut la lire. On préfère donc un verrou de type optimiste dont le concept est assez simple: la table à modifier contient une colonne avec un numéro de version qui s'incréments à chaque modification. Pendant la transaction, on s'assure que ce numéro n'a pas changé entre le moment ou les données ont été récupérées et celui où elles ont été modifiées. On l’appelle optimiste, car on part du principe qu’il n’y aura pas de modification simultanée. La ligne n’est pas verrouillée et l’accès concurrent est autorisé.

Je me permets cette remarque, car dans le cas où la méthodologie pour développer l'application c'est pas du db-first comme le recommande Olivier, et bah c'est souvent le framework (ou l'ORM plus spécifiquement) qui s'en charge. Donc attention quand le verrou choisi par défaut n'est pas celui auquel vous vous attendiez.
Modifié par Anymah (23 Apr 2026 - 05:55)
Modérateur
Merci pour vos retours, c'est exactement le genre d'échange qui fait avancer.

@Olivier C : Excellente suggestion la contrainte CHECK stock >= 0. C'est le filet de sécurité ultime. Même avec toutes les précautions applicatives, la base de données reste le dernier rempart. Je suis totalement d'accord avec l'approche DB-first que tu défends. D'ailleurs, Django permet aussi de poser ce genre de contrainte via models.CheckConstraint dans la classe Meta du modèle, ce qui permet de la versionner dans le code plutôt que de la gérer manuellement en SQL. Dans les frameworks modernes et les dernières versions des bases de données, le fait de déclarer un champ en PositiveIntegerField, ça revient à mettre une contrainte équivalente. Je pense que ceinture, bretelles sont pertinentes dans ce cas. (quoiqu'en dise certain)

En vanilla SQL, ça donne ceci (pour les curieux) :

ALTER TABLE produit ADD CONSTRAINT check_stock_positif CHECK (stock >= 0);


@Anymah : Très juste sur le verrouillage optimiste. Effectivement, dans un scénario où les lectures sont bien plus fréquentes que les écritures, un SELECT FOR UPDATE peut devenir un goulot d'étranglement. Le verrouillage optimiste (colonne version incrémentée) est une alternative élégante, et la plupart des ORM modernes le supportent nativement.

Pour compléter sur la gestion de la charge, l'utilisation de Celery en Python est excellente pour désynchroniser les traitements lourds (envoi d'email, génération de PDF) et ne pas bloquer la réponse HTTP. Dans l'écosystème PHP, il me semble que des solutions équivalentes en PHP existent :

- Laravel Queue pour l'écosystème Laravel (Redis, Beanstalkd, SQS)
- Celery PHP

Cela dit, une file d'attente ne résout pas en soi la race condition. Elle découple le traitement lourd de la requête HTTP, mais le problème de concurrence sur la ressource partagée reste entier. C'est vraiment la combinaison transaction + verrou qui fait le travail.

En tout cas, cet échange montre bien que le sujet est plus profond qu'il n'y paraît. Trois niveaux de protection complémentaires :

1. Applicatif : verrou pessimiste ou optimiste
2. Architectural : files d'attente pour désynchroniser
3. Base de données : contrainte CHECK comme garde-fou ultime
Modifié par Niuxe (23 Apr 2026 - 23:13)
Un jour il faudra que je vous présente le pattern Collector/Dispatcher, qui pourrait être une réponse élégante à toutes ces préoccupations... du moins quand je l'aurais suffisamment maîtrisé (et éprouvé un peu) pour vous le présenter. Il faut déjà que j'en fasse une implémentation concrète, sur le papier en tous les cas, c'est ultra perf'.

Mais cette solution sort un peu des sentiers battus (ceux du mainstream seulement, les systèmes critiques utilisent ces solutions depuis longtemps), elle est irrécupérablement data-first, et concrètement data-driven.

En attendant, je vous présente un petit manifeste de ma composition qui en donne un peu l'idée : Manifeste de la Projection Réactive.
Intéressant ton manifeste Olivier ! J’aime beaucoup l’idée de déplacer entièrement la logique métier dans la base de données.

As-tu imaginé une approche hybride ? Avec la logique qui est entièrement dans la base de données mais la projection qui n’est pas réactive. Les notifications de modification pour propager le changement d’état ne sont pas forcément nécessaires et ça simplifie beaucoup l’architecture.

Edit: Désolé si ça deviens off-topic Smiley biggrin
Modifié par Anymah (26 Apr 2026 - 20:28)
[off-topic]

Merci Anymah,

Le projet final (Rust + Maud) est une couche de rendu AOT, mais la base de données elle-même peut être utilisée avec d'autres backends (PHP, Python, Node.js) pour servir une API ou un site web.

Voici une présentation de la DB en quelques lignes :

Cette base de données est conçue comme un back-end complet encapsulé dans PostgreSQL. Toute la logique métier, la sécurité et l’intégrité y sont implémentées directement, ce qui signifie qu’il n’y a pas de couche ORM ni de code applicatif éparpillé pour valider les données. Côté lecture, tu disposes de vues SQL propres qui reconstituent les objets métier exactement comme une API les renverrait (avec les auteurs, les tags, les prix, etc.). Côté écriture, tu appelles simplement des procédures stockées (create_account, publish_document, create_transaction) qui vérifient les droits, les règles de gestion et préservent l’historique. La base s’assure toute seule qu’un utilisateur ne peut pas écrire n’importe quoi, et tout est versionné et auditable. Résultat : ton code applicatif (quel que soit le langage) devient juste un consommateur léger de cette base, sans se soucier des contraintes métier, et les performances de lecture sont optimales parce que les données « chaudes » sont stockées de manière ultra-compacte, sans colonnes inutiles dans le cache.

Voici le lien direct vers tous les fichiers source : DB postgres, ECS/DOD style
Voici la documentation : documentation technique sur la DB
Désolé pour la documentation, il ne s'agit pour l'instant que d'ADR, il n'y a pas de doc pour l'usager. Je ferais mieux, une fois le tout finalisé. Pour l'instant je suis en plein dev' (et ce n'est pas mon projet du moment)

J'ouvrirais un topic dédié si ça vous intéresse.

[/off-topic]
Modifié par Olivier C (27 Apr 2026 - 12:36)