Un virement débite ton compte, puis le serveur plante avant de créditer le destinataire. L’argent a disparu ? Pas si tu utilises les transactions SQL. Dans ce guide, tu comprends pourquoi elles existent, comment utiliser COMMIT et ROLLBACK en SQL comme en PHP, et comment éviter les pièges de la concurrence : lectures incohérentes, stocks négatifs et interblocages.

🐬 MySQL / InnoDB🔒 ACID↩️ COMMIT & ROLLBACK🐘 PHP PDO⏱️ 20 min de lecture

Une application ne fait presque jamais une seule modification à la fois. Passer une commande, c’est créer la commande, ajouter ses lignes, diminuer le stock et enregistrer le paiement. Si l’une de ces étapes échoue au milieu, ta base se retrouve dans un état incohérent : une commande sans lignes, un stock diminué pour une commande qui n’existe pas…

Transactions SQL : Les transactions SQL (COMMIT, ROLLBACK) : pourquoi et comment
Les transactions SQL (COMMIT, ROLLBACK) : pourquoi et comment

Les transactions SQL résolvent ce problème : elles regroupent plusieurs requêtes en un bloc indivisible. Soit tout réussit, soit rien n’est enregistré. C’est l’une des notions les plus importantes des bases de données, et pourtant beaucoup de développeurs s’en passent… jusqu’au jour où un incident les rattrape.

Transactions SQL : le cycle de vie d’une transaction
Le cycle de vie d’une transaction SQL : START TRANSACTION, les requêtes, puis COMMIT ou ROLLBACK.
🎯
Ce que tu vas apprendre
  • Ce qu’est une transaction et les quatre propriétés ACID.
  • Utiliser START TRANSACTION, COMMIT, ROLLBACK et SAVEPOINT.
  • Gérer les transactions SQL en PHP avec PDO, avec une gestion d’erreurs fiable.
  • Comprendre les niveaux d’isolation et les anomalies qu’ils évitent.
  • Verrouiller des lignes avec SELECT … FOR UPDATE pour éviter les stocks négatifs.
  • Réagir aux interblocages (deadlocks).

Pourquoi les transactions SQL sont indispensables

Prenons le grand classique : un virement de 100 € du compte d’Alice vers celui de Bruno. Il faut deux requêtes :

UPDATE comptes SET solde = solde - 100 WHERE id = 1;  -- débite Alice
UPDATE comptes SET solde = solde + 100 WHERE id = 2;  -- crédite Bruno
SQL

Que se passe-t-il si la connexion est coupée, si le serveur redémarre ou si la seconde requête échoue (contrainte non respectée, compte inexistant) entre les deux ? Alice a perdu 100 €, Bruno n’a rien reçu. Les 100 € se sont évaporés.

Avec une transaction, ce scénario devient impossible :

START TRANSACTION;
UPDATE comptes SET solde = solde - 100 WHERE id = 1;
UPDATE comptes SET solde = solde + 100 WHERE id = 2;
COMMIT;
SQL

Si quoi que ce soit échoue avant le COMMIT, aucune des deux modifications n’est enregistrée.

Les propriétés ACID

Les transactions SQL garantissent quatre propriétés, résumées par l’acronyme ACID :

PropriétéSignificationDans l’exemple du virement
AtomicitéTout ou rien : la transaction est indivisibleLes deux comptes sont modifiés, ou aucun
CohérenceLa base passe d’un état valide à un autre état valideLa somme des soldes reste identique, les contraintes sont respectées
IsolationLes transactions simultanées ne se perturbent pasPersonne ne voit Alice débitée et Bruno pas encore crédité
DurabilitéUne fois validée, la modification survit à une panneAprès le COMMIT, le virement est enregistré, même si le serveur redémarre
⚠️
Vérifie le moteur de stockage de tes tables

Dans MySQL et MariaDB, seules les tables InnoDB gèrent les transactions. Le moteur MyISAM, utilisé par de très anciennes applications, les ignore silencieusement : ROLLBACK ne fait rien. InnoDB est le moteur par défaut depuis MySQL 5.5. Vérifie avec SHOW TABLE STATUS WHERE Name = 'comptes'; (colonne Engine) et convertis si besoin : ALTER TABLE comptes ENGINE = InnoDB;.

Le mode autocommit

Par défaut, MySQL fonctionne en mode autocommit : chaque requête est une transaction à elle seule, validée immédiatement. C’est pourquoi, sans START TRANSACTION, tes UPDATE sont enregistrés aussitôt et ne peuvent pas être annulés.

SELECT @@autocommit;   -- 1 = activé (comportement par défaut)
SQL

START TRANSACTION (ou son synonyme BEGIN) suspend l’autocommit jusqu’au prochain COMMIT ou ROLLBACK.

COMMIT et ROLLBACK en pratique

Crée cette table pour suivre les exemples :

CREATE TABLE comptes (
    id INT PRIMARY KEY,
    titulaire VARCHAR(50) NOT NULL,
    solde DECIMAL(10,2) NOT NULL,
    CONSTRAINT solde_positif CHECK (solde >= 0)
) ENGINE = InnoDB;

INSERT INTO comptes VALUES (1, 'Alice', 500.00), (2, 'Bruno', 200.00);
SQL

Valider avec COMMIT

START TRANSACTION;
UPDATE comptes SET solde = solde - 100 WHERE id = 1;
UPDATE comptes SET solde = solde + 100 WHERE id = 2;
COMMIT;

SELECT * FROM comptes;
SQL
idtitulairesolde
1Alice400.00
2Bruno300.00

Annuler avec ROLLBACK

START TRANSACTION;
UPDATE comptes SET solde = 0 WHERE id = 1;    -- oups, une erreur de manipulation
SELECT * FROM comptes WHERE id = 1;           -- dans la transaction : solde = 0
ROLLBACK;

SELECT * FROM comptes WHERE id = 1;           -- après ROLLBACK : solde = 400.00
SQL

Pendant la transaction, ta session voit ses propres modifications. Les autres connexions, elles, voient toujours l’ancienne valeur (c’est l’isolation). Après ROLLBACK, tout revient comme avant.

💡
Le bon réflexe en production

Avant de lancer un UPDATE ou un DELETE à la main sur une base de production, ouvre une transaction, exécute ta requête, vérifie le résultat avec un SELECT, puis seulement COMMIT. Une clause WHERE oubliée se rattrape alors avec un simple ROLLBACK.

Les SAVEPOINT : annuler une partie seulement

Un point de sauvegarde permet d’annuler une partie de la transaction sans tout abandonner :

START TRANSACTION;
INSERT INTO commandes (id, client_id, total) VALUES (42, 1, 150.00);
SAVEPOINT avant_bonus;

UPDATE clients SET points_fidelite = points_fidelite + 15 WHERE id = 1;
-- Le programme de fidélité pose problème : on annule seulement cette partie
ROLLBACK TO SAVEPOINT avant_bonus;

COMMIT;   -- la commande est enregistrée, sans les points
SQL

Les validations implicites : attention aux requêtes DDL

Certaines instructions provoquent un COMMIT automatique de la transaction en cours, même sans que tu le demandes. C’est le cas des instructions qui modifient la structure de la base (DDL) :

START TRANSACTION;
UPDATE comptes SET solde = 0 WHERE id = 2;
CREATE TABLE journal (id INT);   -- ⚠️ COMMIT implicite ici !
ROLLBACK;                        -- trop tard : la mise à zéro est déjà validée
SQL

CREATE, ALTER, DROP, TRUNCATE, RENAME et LOCK TABLES déclenchent tous un commit implicite dans MySQL. Ne les mélange jamais à des modifications de données dans une transaction.

Les transactions SQL en PHP avec PDO

Dans une application, tu ne tapes pas START TRANSACTION à la main : tu utilises les méthodes de PDO. Voici le modèle à retenir, avec une gestion d’erreurs complète :

<?php
declare(strict_types=1);

$pdo = new PDO('mysql:host=localhost;dbname=banque;charset=utf8mb4', 'app', 'secret', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // indispensable : les erreurs lèvent des exceptions
]);

function virement(PDO $pdo, int $de, int $vers, float $montant): void
{
    if ($montant <= 0) {
        throw new InvalidArgumentException('Le montant doit être positif.');
    }

    $pdo->beginTransaction();
    try {
        $debit = $pdo->prepare('UPDATE comptes SET solde = solde - ? WHERE id = ? AND solde >= ?');
        $debit->execute([$montant, $de, $montant]);
        if ($debit->rowCount() !== 1) {
            throw new RuntimeException('Solde insuffisant ou compte débiteur introuvable.');
        }

        $credit = $pdo->prepare('UPDATE comptes SET solde = solde + ? WHERE id = ?');
        $credit->execute([$montant, $vers]);
        if ($credit->rowCount() !== 1) {
            throw new RuntimeException('Compte créditeur introuvable.');
        }

        $journal = $pdo->prepare('INSERT INTO mouvements (compte_de, compte_vers, montant) VALUES (?, ?, ?)');
        $journal->execute([$de, $vers, $montant]);

        $pdo->commit();
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e; // on laisse l'appelant décider (message à l'utilisateur, journalisation)
    }
}
PHP

Les points clés de ce modèle :

ÉlémentPourquoi
PDO::ERRMODE_EXCEPTIONSans lui, une requête qui échoue renvoie false en silence et la transaction serait validée malgré l’erreur
try / catch (Throwable)On annule pour toute erreur, y compris les erreurs PHP
inTransaction()Évite une exception si la transaction a déjà été terminée
rowCount()Vérifie qu’une ligne a réellement été modifiée (un UPDATE sur un id inexistant ne lève pas d’erreur)
AND solde >= ?Le contrôle du solde est fait dans la requête, de façon atomique
throw $eL’erreur n’est pas masquée : elle remonte pour être gérée et journalisée
💡
Avec un framework, c’est encore plus simple

Laravel propose DB::transaction(function () { ... }), qui fait beginTransaction, commit et rollBack pour toi (et peut même réessayer en cas d’interblocage). Doctrine (Symfony) offre $entityManager->wrapInTransaction(...). Le principe reste exactement le même.

Les niveaux d’isolation

Quand plusieurs utilisateurs travaillent en même temps, leurs transactions s’exécutent en parallèle. Le niveau d’isolation détermine ce qu’une transaction peut voir des modifications des autres. Plus l’isolation est forte, plus les données sont cohérentes, mais plus les transactions risquent de s’attendre mutuellement.

D’abord, les trois anomalies que l’isolation cherche à éviter :

AnomalieCe qui se passe
Lecture sale (dirty read)Tu lis une donnée modifiée par une autre transaction pas encore validée (et qui sera peut-être annulée)
Lecture non répétableTu lis deux fois la même ligne dans ta transaction et obtiens deux valeurs différentes
Lecture fantôme (phantom read)Tu relances la même requête et de nouvelles lignes apparaissent

Et les quatre niveaux d’isolation définis par la norme SQL :

NiveauLecture saleLecture non répétableLecture fantôme
READ UNCOMMITTEDPossiblePossiblePossible
READ COMMITTEDÉvitéePossiblePossible
REPEATABLE READ (défaut InnoDB)ÉvitéeÉvitéeÉvitée dans la plupart des cas (¹)
SERIALIZABLEÉvitéeÉvitéeÉvitée

(¹) InnoDB, grâce à ses lectures cohérentes (snapshots) et à ses verrous sur les intervalles d’index, évite la plupart des lectures fantômes dès REPEATABLE READ, ce qui va au-delà de la norme.

-- Changer le niveau pour la prochaine transaction de cette session
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Voir le niveau actuel
SELECT @@transaction_isolation;
SQL
✅
Garde le niveau par défaut

REPEATABLE READ convient à la grande majorité des applications. Ne change de niveau que pour un besoin précis et compris. PostgreSQL et SQL Server, eux, utilisent READ COMMITTED par défaut : un détail à connaître si tu changes de base de données.

Verrouiller des lignes avec SELECT … FOR UPDATE

L’isolation ne suffit pas toujours. Voici un bug de concurrence très courant : deux clients achètent le dernier article en stock au même moment.

-- Transaction A et transaction B, exactement en même temps
SELECT stock FROM produits WHERE id = 7;      -- les deux lisent : stock = 1
UPDATE produits SET stock = 0 WHERE id = 7;   -- les deux pensent pouvoir acheter
-- Résultat : deux commandes pour un seul article
SQL

Chaque transaction a lu une valeur cohérente… mais déjà dépassée au moment de l’écriture. La solution : verrouiller la ligne au moment de la lecture avec FOR UPDATE. La seconde transaction attend que la première se termine avant de pouvoir la lire.

START TRANSACTION;
SELECT stock FROM produits WHERE id = 7 FOR UPDATE;   -- verrouille la ligne
-- Si stock > 0 : on peut vendre en toute sécurité
UPDATE produits SET stock = stock - 1 WHERE id = 7;
INSERT INTO lignes_commande (commande_id, produit_id, quantite) VALUES (42, 7, 1);
COMMIT;                                                -- libère le verrou
SQL

En PHP :

<?php
$pdo->beginTransaction();
try {
    $stmt = $pdo->prepare('SELECT stock FROM produits WHERE id = ? FOR UPDATE');
    $stmt->execute([$produitId]);
    $stock = (int) $stmt->fetchColumn();

    if ($stock < $quantite) {
        throw new RuntimeException('Stock insuffisant.');
    }

    $pdo->prepare('UPDATE produits SET stock = stock - ? WHERE id = ?')
        ->execute([$quantite, $produitId]);

    $pdo->commit();
} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    throw $e;
}
PHP
💡
L’alternative sans verrou explicite

Pour un cas simple comme celui-ci, une seule requête conditionnelle suffit : UPDATE produits SET stock = stock - 1 WHERE id = 7 AND stock >= 1, puis on vérifie que rowCount() vaut 1. L’opération est atomique, sans SELECT préalable. FOR UPDATE devient indispensable quand la décision dépend de plusieurs lectures.

Les interblocages (deadlocks)

Un interblocage se produit quand deux transactions s’attendent mutuellement : A a verrouillé la ligne 1 et attend la ligne 2, pendant que B a verrouillé la ligne 2 et attend la ligne 1. Aucune ne peut avancer. InnoDB détecte la situation et annule l’une des deux avec l’erreur :

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Texte

Ce n’est pas un bug de MySQL : c’est un événement normal dans une application très sollicitée, à gérer dans ton code. La transaction annulée peut simplement être relancée :

<?php
function avecReessai(PDO $pdo, callable $operation, int $tentatives = 3): mixed
{
    for ($i = 1; ; $i++) {
        $pdo->beginTransaction();
        try {
            $resultat = $operation($pdo);
            $pdo->commit();
            return $resultat;
        } catch (PDOException $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            $interblocage = ($e->errorInfo[1] ?? null) === 1213;
            if (!$interblocage || $i >= $tentatives) {
                throw $e;
            }
            usleep(random_int(50_000, 200_000)); // petite pause aléatoire avant de réessayer
        }
    }
}
PHP
✅
Réduire les interblocages
  • Accède toujours aux tables et aux lignes dans le même ordre (par exemple, par identifiant croissant).
  • Garde des transactions courtes.
  • Indexe les colonnes utilisées dans les WHERE des UPDATE : sans index, InnoDB verrouille beaucoup plus de lignes que nécessaire.

Les bonnes pratiques des transactions SQL

  • Courtes : ouvre la transaction le plus tard possible et termine-la le plus tôt possible. Les verrous sont maintenus jusqu’au COMMIT.
  • Sans attente extérieure : n’appelle jamais une API, n’envoie pas d’e-mail et n’attends pas une action de l’utilisateur au milieu d’une transaction. Fais-le après le COMMIT.
  • Toujours terminées : chaque beginTransaction() doit aboutir à un commit() ou un rollBack(), quoi qu’il arrive (d’où le try/catch).
  • Pas de DDL dans une transaction de données (commit implicite).
  • Les contrôles dans la base : contraintes CHECK, FOREIGN KEY, UNIQUE. Elles font échouer la transaction au bon moment, même si ton code oublie une vérification.
Transactions SQL : les points essentiels à retenir
Les points essentiels à retenir de ce guide.

FAQ

Les transactions SQL ralentissent-elles l’application ?

Très peu, et souvent elles l’accélèrent : regrouper 1 000 INSERT dans une seule transaction est beaucoup plus rapide que 1 000 validations séparées, car le disque n’est synchronisé qu’une fois au COMMIT. Ce qui ralentit, ce sont les transactions longues, qui gardent des verrous.

Que se passe-t-il si la connexion est coupée avant le COMMIT ?

La transaction est automatiquement annulée par le serveur : c’est l’atomicité. Aucune modification partielle n’est enregistrée.

Peut-on imbriquer des transactions ?

Pas dans MySQL : un nouveau START TRANSACTION valide implicitement la transaction en cours. Pour un comportement similaire, utilise les SAVEPOINT. Certains frameworks simulent l’imbrication en créant des points de sauvegarde automatiquement.

Faut-il une transaction pour une seule requête ?

Non : en mode autocommit, chaque requête est déjà atomique. Les transactions deviennent nécessaires dès que plusieurs requêtes doivent réussir ou échouer ensemble.

Conclusion

Les transactions SQL sont la garantie que ta base de données reste cohérente quoi qu’il arrive : panne, erreur, accès simultanés. Retiens le trio START TRANSACTION, COMMIT, ROLLBACK, le modèle PDO avec try/catch et inTransaction(), le verrouillage avec FOR UPDATE pour les stocks et les soldes, et la relance automatique en cas d’interblocage.

Une base de données fiable passe aussi par des requêtes bien écrites : notre guide GROUP BY et HAVING expliqués avec des exemples t’aide à maîtriser l’analyse de tes données. Et pour sécuriser l’ensemble de ton application, jette un œil aux failles de l’OWASP Top 10. La documentation MySQL sur les transactions InnoDB détaille le fonctionnement interne.

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *