Rendre le prêt concurrentiel et cohérent
Objectif : Implémenter et tester une transaction de prêt cohérente face à des demandes simultanées pour le même exemplaire.
Deux adhérents peuvent demander le même exemplaire presque au même moment. Sans coordination, chacun peut constater qu’il est libre et créer un emprunt : votre base enregistre alors deux prêts actifs pour un seul exemplaire.
Vous allez rendre cette décision atomique : la transaction verrouille d’abord la ligne de l’exemplaire, vérifie qu’aucun emprunt n’est actif, puis insère le nouvel emprunt. Le verrouillage avec SELECT ... FOR UPDATE et les règles de reprise après conflit ont été présentés dans les leçons précédentes ; vous les mettez ici à l’épreuve dans le projet.
Préparer la transaction de prêt
Ajoutez le scénario ci-dessous à sql/transactions.sql, après les exemples précédents. Il utilise les tables et colonnes du schéma chargé dans sql/schema.sql et les données de sql/donnees.sql : choisissez un exemplaire existant et deux adhérents existants, libres de tout autre emprunt actif pour cet exemplaire.
-- Remplacez ces identifiants par des lignes existantes et disponibles.
SET @exemplaire_id = 1;
SET @adherent_id = 1;
START TRANSACTION;
-- Sérialise les demandes portant sur cet exemplaire.
SELECT exemplaire_id
FROM exemplaires
WHERE exemplaire_id = @exemplaire_id
FOR UPDATE;
-- Refuse le prêt si un emprunt de cet exemplaire est encore actif.
SELECT emprunt_id
FROM emprunts
WHERE exemplaire_id = @exemplaire_id
AND date_retour IS NULL
LIMIT 1;
-- Exécutez cette insertion seulement si la vérification ne renvoie aucune ligne.
INSERT INTO emprunts (adherent_id, exemplaire_id, date_emprunt, date_retour)
VALUES (@adherent_id, @exemplaire_id, CURRENT_DATE(), NULL);
-- Validez uniquement si le verrou et la vérification ont réussi.
COMMIT;Dans un programme, l’insertion est conditionnelle : la ligne n’est créée que si l’exemplaire a été trouvé et si la vérification ne renvoie aucun emprunt actif. La transaction doit rester ouverte pendant ces étapes. Si une requête échoue, ou si votre contrôle métier refuse le prêt, annulez toute l’opération au lieu de valider une transaction partielle.
Tester deux demandes simultanées
Ouvrez deux connexions à MariaDB, nommées ici session A et session B. Utilisez le même exemplaire_id dans les deux sessions, mais un adherent_id différent ; vérifiez auparavant que l’exemplaire n’a pas de prêt actif. Cette préparation permet d’observer le conflit sans attribuer le résultat à un emprunt préexistant.
- Dans A, démarrez une transaction et exécutez le SELECT sur exemplaires avec FOR UPDATE. Laissez cette transaction ouverte.
- Dans B, démarrez une transaction et exécutez le même SELECT pour le même exemplaire. La requête attend le déverrouillage de A.
- Dans A, vérifiez qu’aucun emprunt actif n’existe, insérez l’emprunt, puis exécutez COMMIT.
- Dans B, la lecture verrouillée reprend après la validation de A. Vérifiez l’emprunt actif : elle doit en trouver un. N’insérez rien et exécutez ROLLBACK.
Après ces opérations, une seule des deux demandes a créé un emprunt actif. Le verrou de A empêche B de prendre sa décision à partir d’un état devenu obsolète ; après l’attente, B effectue son contrôle sur l’état validé par A et refuse le doublon. La demande B se termine proprement par un refus métier, pas par une insertion concurrente.
Contrôler COMMIT et ROLLBACK
Pour vérifier la validation, recherchez l’exemplaire dans emprunts après le COMMIT de la session A : vous devez voir un emprunt avec date_retour IS NULL. Pour vérifier l’annulation, répétez le scénario avec un autre exemplaire disponible, insérez une ligne, puis exécutez ROLLBACK au lieu de COMMIT ; la ligne ne doit pas apparaître dans une nouvelle lecture.
-- Résultat attendu après COMMIT : une ligne pour l’exemplaire prêté.
SELECT emprunt_id, adherent_id, exemplaire_id, date_emprunt
FROM emprunts
WHERE exemplaire_id = @exemplaire_id
AND date_retour IS NULL;Après COMMIT : une ligne pour l’exemplaire prêté. Après ROLLBACK : aucune ligne créée par la transaction annulée.Si l’insertion est annulée, seules les écritures de cette transaction sont retirées ; les lignes qui existaient auparavant restent en place. Réinitialisez ou choisissez un autre exemplaire pour chaque répétition afin que les résultats ne soient pas faussés par un prêt déjà validé. Ces contrôles rendent visibles les effets des deux issues sans modifier la structure des tables.
Reprendre après un conflit
Un conflit peut se terminer par une attente de verrou dépassée ou par un interblocage. Dans ces cas, MariaDB peut annuler tout ou partie de la transaction selon l’erreur et le contexte ; votre application doit traiter l’échec, terminer la transaction dans un état propre, puis recommencer l’opération depuis le début si une nouvelle tentative est justifiée.
- Abandonnez l’essai concerné et rétablissez une connexion dans un état transactionnel connu.
- Rejouez toute l’opération : verrouillage de l’exemplaire, contrôle de disponibilité et insertion éventuelle.
- Ne rejouez pas directement l’INSERT : une autre transaction a pu prêter l’exemplaire pendant l’attente.
- Limitez les tentatives et signalez un refus si le contrôle détecte désormais un emprunt actif.
La reprise doit donc recalculer la décision à partir de l’état actuel, et non supposer que la disponibilité observée avant le conflit est encore vraie. Pour ce projet, conservez le scénario dans sql/transactions.sql et notez les identifiants d’essai utilisés ainsi que le résultat obtenu. Le compte rendu complet et les mesures comparatives seront traités dans la prochaine leçon.
À retenir
- Verrouillez l’exemplaire avant de contrôler sa disponibilité.
- Gardez le verrou, le contrôle et l’insertion dans la même transaction.
- Après COMMIT, vérifiez l’emprunt actif ; après ROLLBACK, vérifiez que l’insertion a disparu.
- Après un conflit, reprenez l’opération complète et refaites le contrôle métier avant toute insertion.