Cours BUT Info

Ch. 5 — L'accès concurrent

Transactions, ACID, anomalies de concurrence, niveaux d'isolation, MVCC et verrous explicites : comment PostgreSQL garde la base cohérente quand plusieurs clients travaillent en même temps.

Jusqu'ici, nos programmes parlaient à la base une requête à la fois, comme si un seul utilisateur était connecté. En réalité, plusieurs clients travaillent simultanément — et un système informatique subit des pannes (matérielles ou logicielles). Une panne en cours d'exécution d'un programme peut avoir des conséquences désastreuses. Ce chapitre explique les mécanismes qui gardent la base cohérente malgré ces deux menaces.

1. Le besoin : tout ou rien

Exemple canonique : on transfère 100 € du compte 1 au compte 2.

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

Si le système tombe entre les deux requêtes, l'argent a disparu. Il faut pouvoir assurer que soit tout réussit, soit rien n'est appliqué — et que la base reste cohérente, même en cas de panne ou d'erreur. La réponse est la transaction :

Une transaction est un ensemble d'opérations SQL (lecture, écriture, suppression…) qui forment une unité logique de travail.

2. La syntaxe : BEGIN, COMMIT, ROLLBACK

Trois ordres SQL suffisent :

OrdreRôle
BEGINdémarre la transaction
COMMITvalide toutes les modifications
ROLLBACKannule tout en cas d'erreur
BEGIN;
-- opérations SQL (SELECT, INSERT, UPDATE…)
COMMIT;

Techniquement, le SGBD s'appuie sur le journal (WAL : Write-Ahead Logging) pour implémenter cela : les modifications sont écrites dans un journal avant d'être appliquées aux données — c'est ce qui rend le ROLLBACK et la reprise après panne possibles.

3. Le virement, en transaction

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

Si une étape échoue, on fait un ROLLBACK pour ne rien perdre : les deux soldes reviennent à leur état d'avant BEGIN.

4. L'autocommit — et le virement en JDBC

Les clients (interactifs ou JDBC) sont en général en mode autocommit : dans ce mode, chaque requête SQL est immédiatement validée à l'exécution. Pour changer cela :

  • dans un client interactif : \set AUTOCOMMIT = 'off' ;
  • avec JDBC : con.setAutoCommit(false);

(Le driver gère souvent implicitement le BEGIN.) En mode autocommit désactivé, il faut faire ses COMMIT et ROLLBACK explicitement. Le virement complet, en JDBC :

Virement.java
public class Virement {
    public static void main(String[] args) throws Exception {
        DataSource ds = ...;
        try (Connection conn = ds.getConnection()) {
            conn.setAutoCommit(false);
            try (
                PreparedStatement debit = conn.prepareStatement(
                        "UPDATE compte SET solde = solde - 100 WHERE id = ?");
                PreparedStatement credit = conn.prepareStatement(
                        "UPDATE compte SET solde = solde + 100 WHERE id = ?")
            ) {
                debit.setInt(1, 1);
                debit.executeUpdate();
                credit.setInt(1, 2);
                credit.executeUpdate();
                conn.commit();
                System.out.println("Transaction validée avec succès.");
            } catch (Exception e2) {
                System.out.println("Pb de transaction");
                conn.rollback();
            }
        } catch (SQLException e1) {
            System.out.println("Pb de connexion");
        }
    }
}

5. Les problèmes de concurrence

Avec une seule transaction, le couple transaction + WAL assure la durabilité et la cohérence. Dès que plusieurs transactions s'exécutent en même temps, d'autres problèmes apparaissent. Le scénario du cours :

-- TRANSACTION A
BEGIN;
SELECT solde FROM comptes WHERE id = 1;              -- 1000 €

-- TRANSACTION B (simultanée)
BEGIN;
UPDATE comptes SET solde = solde - 200 WHERE id = 1;
COMMIT;                                              -- Solde = 800 €

-- POURSUITE DE LA TRANSACTION A
UPDATE comptes SET solde = solde - 100 WHERE id = 1; -- ? 900 € ou 700 € ?
COMMIT;

La réponse dépend du niveau d'isolation… Ce genre de course donne lieu à 4 problèmes classiques :

ProblèmeCe qui se passeExemple
Lecture sale (dirty read)une transaction lit des données non validées par une autre ; si l'autre fait ROLLBACK, les données lues étaient faussesT1 modifie solde = 900 (pas encore COMMIT) ; T2 lit 900 et décide ; T1 fait ROLLBACK → le solde redevient 1000 — T2 a agi sur des données qui n'existent plus !
Lecture non reproductible (non-repeatable read)une transaction lit la même ligne plusieurs fois et obtient des valeurs différentesT1 lit solde = 1000 ; T2 modifie à 800 et COMMIT ; T1 relit : 800. Même transaction, deux résultats différents !
Perte de mise à jour (lost update)deux transactions modifient la même ligne simultanément ; l'une écrase les changements de l'autreT1 et T2 lisent 1000 ; T1 écrit 900 ; T2 écrit 800 → la mise à jour de T1 est écrasée !
Lecture fantôme (phantom read)une transaction lit un ensemble de lignes, puis de nouvelles lignes apparaissent à la relectureT1 compte 3 comptes avec solde > 5000 ; T2 crée un compte à 10000 ; T1 recompte : 4. Un nouveau compte est apparu comme par magie !

6. ACID : les propriétés souhaitées d'un « bon » SGBD

ACID — les quatre garanties attendues d'un « bon » SGBD. Les trois premières se jouent pour une transaction seule ; l'isolation devient la question centrale dès que plusieurs transactions s'exécutent en même temps. PostgreSQL s'appuie sur le journal (WAL : Write-Ahead Logging).

A Atomicité, C Cohérence et D Durabilité concernent déjà une transaction isolée ; la propriété I — Isolation devient essentielle lorsque plusieurs transactions s'exécutent simultanément. C'est tout l'objet de la suite du chapitre.

7. Deux stratégies face aux conflits

StratégieHypothèseMécanisme
1. Les niveaux d'isolationoptimiste… on pense qu'il n'y aura pas de problèmeon autorise la concurrence, et le SGBD détecte les conflits éventuels
2. Les verrous explicitespessimiste… mieux vaut prévenir que guériron bloque certaines transactions pour éviter les conflits

8. Les niveaux d'isolation

L'échelle des niveaux d'isolation : chaque étage hérite des garanties du précédent. Plus on monte, moins les transactions se voient — et plus le SGBD travaille (jusqu'à -50 % de performance en serializable).

Les niveaux déterminent comment les transactions s'influencent entre elles :

NiveauGarantie
Read Uncommittedaucune garantie
Read Committedpas de dirty read
Repeatable Read+ pas de lost update ou de non-repeatable read
Serializable+ pas de phantom read (isolement total)

Le mécanisme, côté PostgreSQL

  • PostgreSQL utilise un mécanisme de snapshot pour maintenir l'isolation : le snapshot garantit de ne pas avoir de dirty read — c'est le niveau read committed ;
  • il utilise un mécanisme MVCC (Multi-Version Concurrency Control) pour garantir le repeatable read : chaque transaction voit sa version des lignes ;
  • il s'appuie enfin sur un graphe de sérialisation et détecte les cycles pour éviter les phantom reads et garantir le serializable ;
  • dès qu'une anomalie est détectée, il rejette la transaction : le SQLState vaut "40001" dans l'exception générée.

Caractéristiques, et la stratégie RETRY

  • on choisit son niveau d'isolation (4 possibles) ;
  • le SGBD rejette les transactions conflictuelles ;
  • le coût en temps est important (jusqu'à -50 % pour serializable) ;
  • stratégie RETRY : on accepte que ça puisse planter, et dans ce cas, on réessaie ;
  • usage idéal : quand les conflits sont rares, que la logique métier est complexe, ou qu'on a besoin de haute performance.
RETRY — réessayer en cas de conflit (SQLState 40001)
int maxTentatives = 3;
for (int tentative = 1; tentative <= maxTentatives; tentative++) {
    try (Connection con = ds.getConnection()) {
        con.setAutoCommit(false);
        con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
        try {
            // opérations de la transaction : SELECT / UPDATE / INSERT …
            con.commit();
            break; // succès : on s'arrête
        } catch (SQLException e) {
            con.rollback();
            if (!"40001".equals(e.getSQLState())) {
                throw e; // ce n'est pas un conflit de concurrence : on s'arrête
            }
            if (tentative == maxTentatives) {
                throw e; // conflit de concurrence, mais on a épuisé nos tentatives
            }
            // sinon : conflit de concurrence → on recommence
        }
    }
}

Les limites des niveaux d'isolation

Serializable est très coûteux → peu utilisé en pratique. Et certains phénomènes métier ne sont pas gérés par les niveaux seuls :

  • le write skew (anomalie logique) : deux transactions lisent des données cohérentes individuellement, mais leur combinaison viole une contrainte métier. Exemple du cours : deux médecins vérifient chacun que l'autre est encore de garde… ils partent tous les deux, et plus personne n'est de garde ;
  • les insertions concurrentes — règle du « 1 personne par siège » :
-- TRANSACTION A (siège 15A)
BEGIN;
SELECT COUNT(*) FROM reservations WHERE siege = '15A' AND vol_id = 123; -- 0 → disponible
INSERT INTO reservations (user_id, vol_id, siege) VALUES (1, 123, '15A');

-- TRANSACTION B (même siège) - SIMULTANÉE
BEGIN;
SELECT COUNT(*) FROM reservations WHERE siege = '15A' AND vol_id = 123; -- 0 → disponible
INSERT INTO reservations (user_id, vol_id, siege) VALUES (2, 123, '15A');
-- Double réservation réussie ! La contrainte UNIQUE échoue trop tard.
  • les contraintes complexes en général.

9. Les verrous explicites

Pour éviter les plantages en série (les rejets du RETRY), on peut poser des verrous explicites. Principe : je verrouille mes données sensibles, et tant que le verrou n'est pas enlevé, tout le monde attend.

L'outil : SELECT … FOR UPDATE.

  • il verrouille les lignes lues en attente d'une modification ;
  • il empêche les mises à jour concurrentes ;
  • il est sûr même en Read Committed ;
  • la transaction qui arrive après doit attendre la levée du verrou.
SELECT … FOR UPDATE : la transaction T1 verrouille les lignes qu'elle lit ; la transaction T2 qui veut les mêmes lignes attend la fin de T1 (COMMIT ou ROLLBACK). C'est le verrou explicite, sûr même en read committed.

Quand l'utiliser :

  • lire + modifier la même ligne dans une même transaction ;
  • réserver une ressource (place, chambre, stock…) ;
  • éviter les lost updates.
BEGIN;
SELECT * FROM comptes WHERE id = 1 FOR UPDATE;
UPDATE comptes SET solde = solde - 100 WHERE id = 1;
COMMIT;

Caractéristiques :

  • ils bloquent les transactions concurrentes ;
  • risque : des deadlocks (interblocages) sont possibles ;
  • usage idéal : quand les conflits sont fréquents, pour des opérations critiques où l'échec est inacceptable, avec une logique simple.

Bonnes pratiques anti-deadlocks :

  • toujours verrouiller les tables dans le même ordre — pas d'ordre inversé dans d'autres programmes ;
  • minimiser le temps entre le verrouillage et le COMMIT / ROLLBACK.

10. Conclusion : deux modes de traitement

Deux manières de voir les choses, complémentaires :

  • l'isolation décrit ce que je peux voir des autres : les niveaux d'isolation déterminent le degré de visibilité des modifications non validées (et donc les phénomènes possibles : lecture sale, lecture non répétable, fantôme…) ;
  • les verrous décrivent quand les autres peuvent voir ce que je fais : ils contrôlent le moment où mes modifications deviennent visibles — ou quand les autres peuvent modifier les mêmes lignes.

Et le résumé du cours, en une arborescence :

TRANSACTION
├── 1 programme           → ATOMICITÉ   → COMMIT / ROLLBACK
└── plusieurs programmes  → CONCURRENCE
        ├── ISOLATION     → RETRY
        └── VERROUS       → ATTENTE

Sans transactions, point de salut !

A retenir

  • une transaction = unité logique de travail : BEGIN … COMMIT, ou ROLLBACK en cas d'échec ; le SGBD s'appuie sur le journal (WAL) ;
  • par défaut, les clients sont en autocommit — on le désactive pour grouper des requêtes (setAutoCommit(false)) ;
  • les 4 anomalies : lecture sale, lecture non reproductible, perte de mise à jour, lecture fantôme ; les ACID sont les garanties attendues en retour ;
  • stratégie optimiste : niveaux d'isolation + RETRY sur SQLState 40001 ; stratégie pessimiste : SELECT … FOR UPDATE + attente — attention aux deadlocks ;
  • en cas de doute sur un « lire puis modifier », verrouillez : c'est le geste qui évite les mises à jour perdues.

cours5-acces-concurrent.pdf

transp_cours305 — L'accès concurrent · 32 diapositives

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page