Les niveaux d'isolation des transactions PostgreSQL en pratique
PostgreSQL implémente trois niveaux d'isolation distincts — Read Committed, Repeatable Read et Serializable. (READ UNCOMMITTED existe pour la compatibilité avec le standard SQL, mais se comporte exactement comme Read Committed : PostgreSQL ne vous montre jamais de données non validées.) Les différences ne comptent qu'en situation de concurrence, c'est-à-dire précisément quand elles sont le plus difficiles à déboguer — il vaut donc mieux les connaître avant l'incident.
Read Committed : le niveau par défaut, et son angle mort
Chaque instruction voit un instantané de la base de données au moment où cette instruction a commencé. Deux conséquences :
- Deux
SELECTidentiques à l'intérieur d'une même transaction peuvent renvoyer des résultats différents si quelqu'un valide entre les deux (lecture non répétable). - Le classique lost update (mise à jour perdue) : deux transactions lisent toutes deux un solde de 100, calculent toutes deux 100 − 10, écrivent toutes deux 90. Un retrait disparaît. Read Committed n'empêche pas ce schéma lorsque la lecture et l'écriture sont des instructions séparées.
Les correctifs idiomatiques à ce niveau :
-- make the read-modify-write atomic in one statement:
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
-- or lock the row at read time:
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
Un détail utile : lorsqu'un UPDATE sous Read Committed trouve une ligne déjà modifiée par une transaction concurrente validée, il ré-évalue sa clause WHERE sur la nouvelle version et poursuit. Vous n'obtenez pas d'erreur — ce qui est pratique, et parfois la source de bogues très subtils.
Repeatable Read : un seul instantané pour toute la transaction
L'instantané est pris à la première requête et conservé jusqu'au commit : chaque instruction voit les mêmes données. Dans PostgreSQL, cela empêche aussi les lectures fantômes (plus strict que ce que le standard SQL exige). Le prix se paie sur les écritures :
ERROR: could not serialize access due to concurrent update
Si vous tentez de modifier une ligne qu'une transaction concurrente a modifiée et validée après la prise de votre instantané, PostgreSQL ne peut pas faire comme si les deux ordres avaient eu lieu — il vous avorte. Votre application doit être prête à rejouer l'intégralité de la transaction. Repeatable Read est le niveau naturel pour des lectures cohérentes multi-requêtes : rapports, exports, sauvegardes (pg_dump s'exécute à ce niveau).
Serializable : comme si les transactions s'exécutaient une à la fois
Serializable dans PostgreSQL, c'est Repeatable Read plus le SSI (serializable snapshot isolation) : le moteur suit les dépendances lecture/écriture entre transactions concurrentes et en avorte une dès qu'un cycle rendrait le résultat impossible sous n'importe quel ordre sériel. Il détecte des anomalies que Repeatable Read ne peut pas — le cas d'école est le write skew (anomalie d'écriture asymétrique) :
-- Two doctors, rule: at least one must stay on call.
-- T1: SELECT count(*) FROM oncall; -- sees 2, so it's safe to leave
-- T2: SELECT count(*) FROM oncall; -- sees 2, so it's safe to leave
-- T1: DELETE FROM oncall WHERE doctor = 'alice'; COMMIT;
-- T2: DELETE FROM oncall WHERE doctor = 'bob'; COMMIT;
-- Repeatable Read: both commit, zero doctors on call.
-- Serializable: one of them gets error 40001 and must retry.
Coûts : un peu de CPU et de mémoire pour le suivi des dépendances, et — surtout — des faux positifs : des transactions peuvent être avortées « par précaution ». Toutes les transactions de la charge de travail doivent s'exécuter en Serializable pour que ses garanties tiennent, et chacune d'elles a besoin d'une logique de retry.
Rejouer correctement
- Rejouez sur les SQLSTATE
40001(serialization_failure) et40P01(deadlock_detected). - Rejouez la transaction entière, y compris ses lectures — les valeurs que vous avez lues la première fois sont exactement celles qui peuvent ne plus être vraies. Ne rejouer que l'instruction en échec est une erreur.
- Gardez les transactions rejouées courtes et bornées (nombre de tentatives limité, petit backoff). Une transaction qui lit la moitié de la base entrera en collision indéfiniment.
- N'enveloppez pas d'effets de bord (e-mails, appels HTTP) à l'intérieur d'une transaction que vous comptez rejouer.
Choisir en pratique
| Situation | Choix raisonnable |
|---|---|
| OLTP typique avec mises à jour de lignes ciblées | Read Committed + mises à jour atomiques / SELECT FOR UPDATE là où c'est important |
| Lectures multi-instructions devant être mutuellement cohérentes | Repeatable Read |
| Invariants s'étendant sur plusieurs lignes (« au moins un », « la somme ne doit pas dépasser », double réservation) | Serializable avec retries — ou verrouillage explicite si les points chauds sont peu nombreux et connus |
| Files de tâches | Read Committed + FOR UPDATE SKIP LOCKED |
Définissez le niveau par transaction (BEGIN ISOLATION LEVEL SERIALIZABLE) ou par session ; mélanger les niveaux ne pose pas de problème, sauf que, comme indiqué, les garanties de Serializable exigent que toutes les transactions liées l'utilisent.
