Introduction
Lorsqu'une application backend sert de nombreux utilisateurs en même temps, la base de données devient le champ de bataille central de la cohérence. Deux utilisateurs qui réservent la dernière place, une confirmation de paiement qui rivalise avec un remboursement, un rapport qui lit des données pendant qu'une mise à jour est en cours — chacun de ces scénarios touche au mécanisme de transactions et de concurrence de PostgreSQL. Se tromper conduit à une corruption silencieuse des données, à des commandes dupliquées ou à des soldes impossibles.
Cette leçon se concentre sur ce qu'un développeur backend doit savoir pour raisonner sur le comportement concurrent : comment PostgreSQL encapsule le travail dans des transactions, comment les niveaux d'isolation modifient la visibilité entre transactions concurrentes, comment le MVCC permet aux lecteurs et aux writers de ne pas se bloquer mutuellement, et où les verrous au niveau des lignes s'engagent réellement. L'objectif n'est pas la profondeur d'administration de bases de données, mais le modèle mental pratique nécessaire lorsqu'un rapport de bug en production dit « deux requêtes ont fait la même chose » ou « le tableau de bord affichait des chiffres périmés ».
ACID et frontières de transaction
Une transaction dans PostgreSQL est une unité de travail qui soit commit complètement, soit rollback complètement. Les quatre garanties — Atomicité, Cohérence, Isolation, Durabilité — décrivent ce que la base de données promet si vous enveloppez vos instructions dans BEGIN et COMMIT.
Dans un backend, la forme typique ressemble à ceci :
Si le second UPDATE échoue, le premier est aussi annulé. Il n'y a jamais d'état où l'argent a été débité mais pas crédité. L'atomicité ici n'est pas un nice-to-have — c'est ce qui maintient les comptes équilibrés.
Un point pratique pour les développeurs backend : PostgreSQL traite chaque instruction individuelle comme sa propre transaction implicite sauf si vous faites explicitement BEGIN. Donc si vous émettez deux instructions UPDATE séparées sans bloc de transaction, chacune commit indépendamment. C'est la cause racine de nombreux bugs « partiellement appliqués » dans le code applicatif qui parle à la base de données via un connection pooler ou un ORM avec le paramètre autocommit par défaut.
Niveaux d'isolation dans PostgreSQL
Le SQL définit quatre niveaux d'isolation : READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ et SERIALIZABLE. PostgreSQL en implémente trois comme des comportements réellement distincts — READ UNCOMMITTED se comporte comme READ COMMITTED à cause du fonctionnement interne du MVCC.
Le niveau que vous choisissez décide ce qu'une transaction a le droit de voir des transactions concurrentes. READ COMMITTED est le niveau par défaut et est généralement ce que vous voulez : chaque instruction voit les données commitées avant que cette instruction ne commence. Si une autre transaction commit une mise à jour entre vos deux SELECT, le second SELECT verra la nouvelle valeur — ce qui peut surprendre les gens qui attendent une vue stable.
REPEATABLE READ verrouille le snapshot au moment où la transaction démarre, et non à chaque instruction. SERIALIZABLE va un cran plus loin : il détecte les violations de sérialisabilité et annule l'une des transactions en conflit avec une erreur de serialization failure que l'application doit être prête à retenter.

MVCC et verrous au niveau des lignes
PostgreSQL utilise le Multi-Version Concurrency Control. Quand vous faites UPDATE sur une ligne, la base de données n'écrase pas l'ancienne version en place. Elle écrit une nouvelle version de la ligne et marque l'ancienne comme morte. Les lecteurs voient la version qui a été commitée au moment où leur snapshot a été pris. C'est pourquoi une longue requête de reporting ne bloque pas les écritures entrantes, et vice versa — elles opèrent sur des versions de lignes différentes.
Mais le MVCC n'élimine pas tout blocage. Quand deux transactions essaient de faire UPDATE sur la même ligne, PostgreSQL a besoin d'un gagnant, et le perdant attend sur un verrou au niveau de la ligne jusqu'à ce que la première transaction commit ou rollback. SELECT FOR UPDATE est la façon explicite de revendiquer ce verrou avant de faire une logique métier qui dépend de la valeur courante.
Le pattern SELECT FOR UPDATE suivi d'un UPDATE conditionnel est le correctif standard pour le race condition classique où deux requêtes lisent chacune la valeur, décident toutes les deux que c'est sûr, et décrémentent toutes les deux. Sans le verrou explicite, les deux transactions lisent le même stock original et vous survendez.
Pièges courants et enseignements pratiques
Trois modes de défaillance reviennent constamment dans le code backend. Premièrement, les transactions longues détiennent des snapshots et empêchent le nettoyage des tuples, donc la table grossit et les performances se dégradent avec le temps — fermez les transactions rapidement, n'en ouvrez pas une au début d'une requête HTTP et ne la laissez pas ouverte à travers des appels en aval. Deuxièmement, les deadlocks arrivent quand deux transactions acquièrent des verrous dans des ordres opposés ; gardez l'ordre cohérent dans tout le codebase, ou fiez-vous à PostgreSQL pour détecter et annuler l'une d'elles puis retenter. Troisièmement, ignorer les serialization failures sous SERIALIZABLE conduit à une corruption des données — la base de données vous dit que les opérations n'étaient en fait pas sûres à exécuter en parallèle, et votre logique de retry fait partie du contrat, elle n'est pas optionnelle.
L'état d'esprit pratique : traitez le choix du niveau d'isolation et du verrou comme une partie de la conception applicative, pas comme un détail de tuning de base de données. Le READ COMMITTED par défaut avec un SELECT FOR UPDATE explicite autour des sections critiques résout proprement la plupart des bugs de concurrence. Optez pour SERIALIZABLE quand la correctness compte plus que le throughput et que vous pouvez gérer les défaillances.
Résumé
Les transactions regroupent plusieurs instructions dans une unité atomique. Les niveaux d'isolation contrôlent ce que les transactions concurrentes peuvent voir du travail non commité de chacune. Le MVCC donne à PostgreSQL des lectures et écritures non bloquantes en conservant plusieurs versions de lignes, tandis que les verrous au niveau des lignes résolvent les conflits quand deux transactions ciblent la même ligne. Pour les développeurs backend, les outils concrets sont la discipline BEGIN/COMMIT, le pattern SELECT FOR UPDATE pour les mises à jour sujettes aux race conditions, et un ordre de verrouillage cohérent pour éviter les deadlocks. Choisissez le niveau d'isolation délibérément — le défaut fonctionne pour la plupart des cas, mais REPEATABLE READ et SERIALIZABLE existent pour les situations où le comportement du défaut n'est pas assez strict.
Point de contrôle de la leçon
Révisez la leçon et recevez des conseils sur votre travail.