Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Auf Deutsch lesen

Section 7.3 sur 7.8 · Avancé · 28 min de lecture

Transactions, ACID et niveaux d’isolation

Leçon vidéo

5 min · narré en anglais, avec sous-titres
Écouter seulement (audio)
Lire la transcription

Les bugs de concurrence passent tous les tests sur une machine tranquille, puis apparaissent au pic de l’heure du dîner. Dans cette leçon, vous allez découvrir ce que les transactions garantissent vraiment quand beaucoup s’exécutent à la fois, et comment protéger vos règles.

Imaginez deux caissiers qui partagent un même registre papier pour le compte d’un client. Chacun consulte le solde, calcule le nouveau sur un bloc-notes, et l’inscrit. S’ils le font au même moment, la modification de l’un des caissiers est perdue. Une transaction s’accompagne de règles sur la façon dont les caissiers passent chacun leur tour : peuvent-ils jeter un œil aux écritures à moitié rédigées de l’autre, et l’un doit-il attendre ? Des règles plus strictes signifient moins d’erreurs mais plus d’attente. Et, comme vous allez le voir, les noms de ces règles n’ont pas tout à fait le même sens d’une base de données à l’autre.

Une transaction regroupe des lectures et des écritures en une seule unité, et vous avez sans doute déjà entendu l’acronyme ACID. L’atomicité concerne les défaillances : si quelque chose tourne mal en cours de route, toutes les modifications sont annulées, et vous pouvez donc réessayer sans risque. La cohérence signifie que vos règles sont respectées, par exemple que les débits égalent les crédits. La base de données applique les clés et les contraintes, mais la plupart des règles métier ne sont maintenues que par vos propres transactions. L’isolation signifie que les transactions concurrentes n’interfèrent pas entre elles. Dans sa forme la plus stricte, dite sérialisable, le résultat est le même que si elles s’étaient exécutées une par une. Les niveaux plus faibles tolèrent certaines interférences, pour gagner en vitesse. Et la durabilité signifie qu’une fois un commit confirmé, les données survivent à une panne.

Le bug de concurrence le plus courant dans le code métier est la mise à jour perdue. Ici, deux retraits frappent un compte qui contient cent livres. La première transaction, qui retire trente, lit le solde et voit cent. Un instant plus tard, la seconde, qui retire cinquante, lit elle aussi cent. La première écrit soixante-dix : cent moins trente. Puis la seconde écrit cinquante, cent moins cinquante, ce qui écrase sans bruit la première écriture. Le solde devrait être de vingt. Un retrait a été perdu, alors que chaque transaction était correcte prise isolément.

La plupart des bases de données modernes donnent à chaque transaction un instantané cohérent, grâce au contrôle de concurrence multiversion. Au lieu d’écraser une ligne, chaque écriture crée une nouvelle version, marquée du numéro de la transaction qui l’a écrite. Un instantané pris à la transaction cent cinq voit la version la plus récente validée avant ce moment : un solde de soixante-dix. Un instantané ultérieur, à cent vingt, voit vingt. Ainsi, les lecteurs n’attendent jamais les écrivains, et les écrivains n’attendent jamais les lecteurs. Le prix à payer, c’est l’entretien : une transaction laissée ouverte pendant des heures maintient en vie les anciennes versions, et le stockage gonfle.

Lire dans un instantané s’appelle l’isolation par instantané. Elle empêche les mises à jour perdues, mais elle permet une anomalie plus subtile appelée write skew, si bien qu’elle n’est pas pleinement sérialisable. Les noms des niveaux d’isolation ne sont pas portables, et les valeurs par défaut diffèrent. PostgreSQL et Oracle utilisent par défaut read committed, MySQL repeatable read, et CockroachDB serializable. Pire, repeatable read ne signifie pas la même chose dans PostgreSQL et dans MySQL. Celui de MySQL peut encore perdre une mise à jour écrite sous la forme d’une lecture suivie d’une écriture. Et le serializable d’Oracle est en réalité de l’isolation par instantané. Un code correct sur une base de données peut être faux sur une autre, avec un paramètre portant exactement le même nom.

Voici le write skew en action. Le planning de garde d’un hôpital a une seule règle : au moins un médecin doit être de garde. Alice et Bob sont de garde, et tous deux appuient sur « Quitter la garde » presque au même moment. Chaque demande compte les médecins de garde. S’il y en a au moins deux, elle fait passer sa propre ligne à « pas de garde ». En isolation par instantané, les deux instantanés montrent deux médecins de garde, donc les deux vérifications passent. La transaction d’Alice met à jour la ligne d’Alice, et celle de Bob la ligne de Bob. Ce sont des lignes différentes, donc aucun conflit n’est détecté. Les deux sont validées, et plus personne n’est de garde. En isolation sérialisable, la base de données remarque que chaque transaction s’est appuyée sur des données que l’autre a ensuite modifiées. L’un des commits échoue, et la nouvelle tentative recompte, ne trouve plus qu’un médecin, et refuse. À un niveau plus faible, vous pouvez verrouiller les lignes dont dépend la décision, avec select for update. La demande de Bob attend alors que celle d’Alice se termine.

Tout code qui lit une valeur, décide dans l’application et écrit en retour est à risque. Nommez la règle qu’il protège, et laissez les contraintes ou une seule mise à jour conditionnelle faire le travail quand c’est possible. Le verrouillage pessimiste verrouille les lignes que vous lisez jusqu’à votre commit. Il convient aux opérations courtes et très sollicitées, comme affecter un livreur à une commande, au prix de l’attente et d’éventuels interblocages. Le contrôle optimiste ne prend aucun verrou. Chaque ligne porte un numéro de version, et l’écriture ne réussit que si la version n’a pas changé. Il convient au gérant qui modifie une carte dans un navigateur pendant cinq minutes. Ou bien utilisez l’isolation sérialisable, et relancez toute la transaction quand elle échoue : gardez donc les e-mails et les appels de paiement en dehors d’elle. Les praticiens ne s’accordent pas sur le meilleur choix par défaut ; ce qui compte, c’est un choix délibéré et testé.

La section complète présente toutes les anomalies, une comparaison des bases de données réelles et une liste de contrôle. Ensuite, dans la section sept point quatre, nous verrons la réplication et le partitionnement. À très vite.

Ce qu’il faut savoir

Ce que promet une transaction

Une transaction regroupe plusieurs lectures et écritures en une seule unité qui prend effet entièrement (elle est validée, commit) ou pas du tout (elle est abandonnée et annulée, rollback). Le chapitre 0 a présenté l’idée avec un virement bancaire. Cette section porte sur ce que la plupart des développeurs comprennent mal : ce qui se passe quand de nombreuses transactions s’exécutent en même temps, quelles garanties les bases de données donnent réellement, et comment choisir. Les exemples utilisent SQL, mais le raisonnement vaut pour tout stockage qui propose des transactions.

Les transactions comptent pour l’architecture parce qu’elles déterminent où les règles métier peuvent être appliquées à moindre coût. Au sein d’une seule base de données, une transaction peut maintenir un registre équilibré ou empêcher qu’une table soit réservée deux fois, en quelques lignes de code. Entre services, cet outil n’existe pas, et c’est pourquoi la propriété des données et les sagas sont si importantes. Savoir exactement ce que garantit une seule base de données vous dit quelles règles doivent rester à l’intérieur du périmètre d’un seul propriétaire.

ACID en détail

L’acronyme ACID a été forgé par Theo Härder et Andreas Reuter en 1983, à partir des travaux antérieurs de Jim Gray. Chaque lettre est plus étroite qu’elle n’en a l’air. L’atomicité concerne les défaillances, pas la concurrence : si quelque chose tourne mal en cours de route (une panne, une violation de contrainte, un interblocage, ou l’application qui décide d’abandonner), toutes les modifications faites jusque-là sont annulées, si bien que l’application peut réessayer sans risque. Abandonnabilité serait un nom plus exact.

La cohérence signifie qu’une transaction fait passer la base de données d’un état valide à un autre, où valide signifie que vos invariants sont respectés : les débits égalent les crédits, le stock n’est jamais négatif, chaque commande a un client. La base de données peut appliquer elle-même certains invariants (clés, clés étrangères, contraintes CHECK, index uniques), mais la plupart sont des règles métier que seules les transactions de l’application maintiennent. La cohérence est donc une propriété de la façon dont l’application utilise l’atomicité et l’isolation, et non quelque chose que la base de données fournit à elle seule. Elle n’a d’ailleurs aucun rapport avec le C de CAP, qui désigne la linéarisabilité (chapitre 6).

L’isolation signifie que des transactions qui s’exécutent simultanément n’interfèrent pas entre elles. Dans sa forme la plus stricte, la sérialisabilité, le résultat de l’exécution concurrente des transactions est le même que si elles s’étaient exécutées l’une après l’autre dans un certain ordre. Les niveaux plus faibles tolèrent certaines interférences en échange de performances, et le reste de cette section explique précisément lesquelles. La durabilité signifie qu’une fois un commit confirmé, les données survivent aux pannes. Les bases de données à un seul nœud y parviennent grâce à un journal d’écriture anticipée écrit sur disque avant que le commit ne rende la main ; les bases de données répliquées y ajoutent des copies sur d’autres machines. La durabilité n’est jamais absolue : un disque peut mentir sur une écriture forcée, et la réplication asynchrone peut perdre les dernières écritures confirmées quand le leader tombe en panne (voir la réplication).

Ce qui tourne mal sans isolation

Les bugs de concurrence sont difficiles à trouver parce qu’ils dépendent du moment : ils passent tous les tests sur la machine tranquille d’un développeur et apparaissent au pic de l’heure du dîner. La façon la plus claire de comprendre les niveaux d’isolation est de passer par les anomalies précises (aussi appelées phénomènes) qu’ils permettent ou empêchent. La mise à jour perdue, ci-dessous, est la plus courante dans le code métier.

Une mise à jour perdue, étape par étape

Deux transactions lisent un solde de 100, chacune soustrait son retrait et réécrit le résultat : la seconde écriture écrase la première et un retrait est perduTransaction A (retrait de 30)Transaction B (retrait de 50)tempslit le soldevoit 100écrit 70100 - 30lit le soldevoit 100écrit 50100 - 50les deux : 100solde 50 au lieu de 20
Chaque transaction est correcte prise isolément ; entrelacées, la seconde écriture efface sans bruit le premier retrait.

Les anomalies, de la plus bénigne à la plus subtile

AnomalieCe qui se passeExemple de livraison de repas ou bancaire
Écriture sale (dirty write)Une transaction écrase des données qu’une autre transaction a écrites mais pas encore validées.Deux transactions qui affectent un livreur s’entrelacent, si bien que la ligne de la commande désigne un livreur et la ligne du livreur une autre commande. Tous les niveaux d’isolation réels empêchent cela.
Lecture sale (dirty read)Une transaction lit des données écrites par une autre transaction qui n’a pas été validée (et qui peut être annulée).Un rapport comptabilise un paiement de 500 qui est ensuite annulé.
Lecture non répétable (fuzzy read)Une transaction lit deux fois la même ligne et obtient des valeurs différentes, parce qu’une autre transaction a validé une modification entre-temps.L’étape de paiement lit le prix d’un plat à 9,50, une modification du restaurant fixant 11,00 est validée, et la seconde lecture de l’étape de paiement voit 11,00.
Read skew (biais de lecture)Une transaction voit des parties de la base de données à des instants différents, si bien que des valeurs liées ne concordent pas.Un client qui a 500 répartis sur deux comptes voit 450 au total, parce qu’un virement de 50 a été validé entre la lecture des deux soldes.
Lecture fantôme (phantom read)Une transaction relance une requête avec une condition et trouve des lignes ajoutées ou supprimées par une autre transaction validée.Compter deux fois les livraisons en cours d’un livreur renvoie 2, puis 3.
Mise à jour perdue (lost update)Deux transactions lisent une valeur, chacune en calcule une nouvelle et l’écrit, et l’une des écritures écrase l’autre sans bruit.Deux retraits sur le même compte (schéma ci-dessus).
Write skew (biais d’écriture)Deux transactions lisent un ensemble de données qui se recoupent, prennent des décisions à partir de celles-ci et mettent à jour des lignes différentes, si bien qu’ensemble elles enfreignent une règle que chacune avait vérifiée.Deux médecins de garde voient chacun que quelqu’un d’autre est encore de garde, et tous deux quittent leur garde (exemple commenté ci-dessous).

Les niveaux d’isolation de la norme SQL

La norme SQL (à partir de SQL-92) définit quatre niveaux d’isolation selon les phénomènes qu’ils interdisent parmi trois : les lectures sales, les lectures non répétables et les fantômes. La norme fait de serializable le niveau par défaut, mais presque aucun produit courant ne la suit sur ce point ; la plupart utilisent par défaut read committed ou repeatable read, pour les performances.

Les niveaux de la norme SQL et les phénomènes qu’ils excluent

Niveau d’isolationLecture sale (dirty read)Lecture non répétableLecture fantôme (phantom read)
Read uncommittedpossiblepossiblepossible
Read committedempêchéepossiblepossible
Repeatable readempêchéeempêchéepossible
Serializableempêchéeempêchéeempêchée, et le résultat doit équivaloir à un certain ordre d’exécution en série

En 1995, Hal Berenson, Phil Bernstein, Jim Gray, Jim Melton, Elizabeth O’Neil et Patrick O’Neil ont publié A Critique of ANSI SQL Isolation Levels. Ils ont montré que les définitions de la norme, rédigées en anglais courant, étaient ambiguës, qu’interdire les trois phénomènes ne suffit pas à garantir la sérialisabilité, et que les niveaux de la norme ne décrivaient pas les niveaux d’isolation qu’utilisaient les produits réels. Ils ont ajouté des phénomènes que la norme avait oubliés, notamment l’écriture sale, la mise à jour perdue, le read skew et le write skew, et ont défini l’isolation par instantané (snapshot isolation) : chaque transaction lit dans un instantané cohérent des données validées tel qu’il était à son début, et une transaction ne peut pas valider l’écriture d’une ligne qu’une transaction concurrente a déjà modifiée et validée (le premier qui valide l’emporte). L’isolation par instantané empêche les lectures sales, les lectures non répétables, le read skew et les mises à jour perdues, ainsi que les fantômes qui touchent les lectures, mais elle permet le write skew : elle n’est donc pas sérialisable. Elle s’insère mal entre les niveaux de la norme, et c’est pourquoi les produits l’étiquettent différemment.

MVCC : comment fonctionnent les instantanés

La plupart des bases de données modernes mettent en œuvre les lectures sur instantané avec le contrôle de concurrence multiversion (MVCC). Au lieu d’écraser une ligne sur place, une écriture crée une nouvelle version marquée de l’identifiant de la transaction qui l’écrit, et l’ancienne version est conservée jusqu’à ce qu’aucune transaction en cours ne puisse encore en avoir besoin. Chaque lecteur reçoit un instantané, c’est-à-dire en substance la liste des transactions qui avaient été validées à son démarrage, et ne voit que les versions créées par ces transactions. Le grand avantage est que les lecteurs n’attendent jamais les écrivains et que les écrivains n’attendent jamais les lecteurs ; seuls deux écrivains sur la même ligne entrent en conflit.

Les versions d’une ligne avec le MVCC

Une ligne a plusieurs versions portant des numéros de transaction de création et d’expiration ; un instantané pris à la transaction 105 voit la version créée par 100, un instantané ultérieur voit celle créée par 110Ligne : compte 7 (MVCC garde plusieurs versions)solde = 100créée par tx 90expirée par tx 100solde = 70créée par tx 100expirée par tx 110solde = 20créée par tx 110version actuelleordre des tx90100110instantané à 105instantané à 120voit 70voit 20un lecteur voit 70 pendant que tx 110 écrit 20 :les lecteurs ne bloquent pas les écrivains, et inversement
Chaque mise à jour crée une nouvelle version de la ligne ; une transaction voit la version la plus récente validée avant son instantané.

Le prix à payer, c’est l’entretien. Les anciennes versions doivent être nettoyées : PostgreSQL les garde dans la table et les supprime avec vacuum, tandis qu’Oracle et MySQL InnoDB conservent les images avant modification dans des journaux d’annulation (undo logs). Une transaction laissée ouverte pendant des heures retient les anciennes versions, si bien que les tables et les journaux d’annulation gonflent et que les performances se dégradent. Les transactions de longue durée sont donc un signal d’alerte opérationnel dans toute base de données MVCC, et une raison de tenir les requêtes analytiques à l’écart de la base de données opérationnelle. En read committed avec MVCC, chaque instruction reçoit un nouvel instantané ; en isolation par instantané, toute la transaction en utilise un seul.

Les valeurs par défaut réelles, et pourquoi les noms induisent en erreur

Les noms des niveaux d’isolation ne sont pas portables. Le même mot peut désigner des garanties différentes selon les produits, et les valeurs par défaut diffèrent. Le projet Hermitage de Martin Kleppmann a testé le comportement réel de nombreuses bases de données face aux anomalies ci-dessus ; la documentation des éditeurs confirme les valeurs par défaut. Considérez ce tableau comme un point de départ et vérifiez la documentation de la version que vous utilisez.

Mêmes noms, comportements différents : ce que font les bases de données réelles (résumé de la documentation des éditeurs et des tests Hermitage)

BaseNiveau par défautCe que sont réellement les niveauxRemarques
PostgreSQLRead committedRead uncommitted se comporte comme read committed. Repeatable read est de l’isolation par instantané. Serializable est de l’isolation par instantané sérialisable (SSI).Repeatable read abandonne une transaction qui tente de mettre à jour une ligne modifiée depuis son instantané (could not serialize access due to concurrent update), si bien que les mises à jour perdues sont empêchées ; le write skew ne l’est pas.
MySQL (InnoDB)Repeatable readLes SELECT simples lisent un instantané pris à la première lecture ; les lectures avec verrou, UPDATE et DELETE agissent sur les dernières lignes validées, avec des verrous d’enregistrement et des verrous d’intervalle (gap locks). Serializable transforme les SELECT simples en lectures avec verrou lorsque l’autocommit est désactivé.En repeatable read, une séquence lecture-modification-écriture écrite sous la forme d’un SELECT puis d’un UPDATE peut encore perdre une mise à jour ou produire un write skew, sauf si le SELECT utilise FOR UPDATE.
Oracle DatabaseRead committedPropose read committed et serializable ; son serializable est de l’isolation par instantané.Empêche les mises à jour perdues en serializable, mais permet le write skew.
Microsoft SQL ServerRead committed (avec verrous) sur SQL Server ; Azure SQL Database active read committed snapshot par défautRead committed avec verrous, read committed snapshot, repeatable read (avec verrous), snapshot (isolation par instantané) et serializable (avec verrous).Sur SQL Server, l’isolation par instantané doit être activée base par base (ALLOW_SNAPSHOT_ISOLATION) avant d’être utilisée.
CockroachDBSerializableSerializable par défaut ; read committed est également disponible.Les clients doivent relancer les transactions qui échouent avec une erreur de sérialisation.

Obtenir la sérialisabilité

Les bases de données qui offrent une véritable sérialisabilité utilisent l’une de trois techniques. L’exécution réellement en série exécute les transactions une par une sur un seul thread, ce qui est rapide si chaque transaction est courte et si les données sont en mémoire (VoltDB, ainsi que les scripts et transactions Redis, fonctionnent ainsi). Le verrouillage à deux phases (2PL), utilisé par les niveaux serializable de SQL Server et de MySQL, fait prendre aux lecteurs des verrous partagés et aux écrivains des verrous exclusifs, y compris des verrous sur des plages pour arrêter les fantômes, et les conserve jusqu’à la fin de la transaction ; c’est correct, mais lecteurs et écrivains se bloquent mutuellement, le débit chute en cas de contention et les interblocages sont fréquents. L’isolation par instantané sérialisable (SSI), décrite par Michael Cahill, Uwe Röhm et Alan Fekete en 2008 et utilisée dans PostgreSQL depuis la version 9.1 (CockroachDB utilise une technique optimiste apparentée), exécute les transactions sur des instantanés sans blocage, suit les dépendances lecture-écriture entre elles et abandonne une transaction quand un motif dangereux apparaît.

Quelle que soit la technique, les transactions sérialisables peuvent échouer avec une erreur de sérialisation (SQLSTATE 40001 dans PostgreSQL et CockroachDB) ou un interblocage, et l’application doit alors relancer toute la transaction. C’est le coût caché du niveau le plus fort : chaque chemin de code transactionnel a besoin d’une boucle de nouvelles tentatives qui puisse être répétée sans risque, ce qui exclut tout effet de bord, comme l’envoi d’e-mails ou l’appel de prestataires de paiement, à l’intérieur de la transaction.

Contrôle de concurrence pessimiste et optimiste

En pratique, la plupart des applications s’exécutent en read committed ou en repeatable read et protègent explicitement certains chemins lecture-modification-écriture. Il existe trois outils. Le meilleur, quand c’est possible, est une mise à jour atomique qui laisse la base de données faire le calcul et la vérification en une seule instruction, comme UPDATE accounts SET balance = balance - 30 WHERE id = 7 AND balance >= 30, puis vérifier qu’exactement une ligne a changé. En read committed dans PostgreSQL, une mise à jour concurrente fait attendre la seconde instruction, qui réévalue ensuite sa condition sur la nouvelle ligne : aucune mise à jour n’est perdue.

Lorsque la décision exige une logique que la base de données ne peut pas exprimer, utilisez le verrouillage pessimiste : verrouillez les lignes que vous lisez avec SELECT ... FOR UPDATE, afin que les transactions concurrentes qui en ont besoin attendent votre commit. C’est simple et adapté aux opérations courtes et très disputées (affecter un livreur à une commande). Les coûts sont l’attente, le risque d’interblocages (verrouillez les lignes dans un ordre constant, par exemple par id croissant) et un débit réduit. Parmi les variantes figurent NOWAIT, qui échoue immédiatement au lieu d’attendre, et SKIP LOCKED, qui saute les lignes déjà verrouillées et constitue la façon classique de construire une file de tâches fiable sur une table relationnelle.

BEGIN;
-- Lock the row: other FOR UPDATE readers and writers must wait
SELECT balance FROM accounts WHERE id = 7 FOR UPDATE;
-- application checks: balance (100) >= 30, so allow it
UPDATE accounts SET balance = 70 WHERE id = 7;
INSERT INTO ledger (account_id, amount, kind)
  VALUES (7, -30, 'withdrawal');
COMMIT;  -- lock released; the waiting transaction now sees 70
Verrouillage pessimiste : le second retrait attend au SELECT que le premier soit validé, puis lit le nouveau solde.

Le contrôle de concurrence optimiste ne prend aucun verrou pendant que l’utilisateur ou le code réfléchit. Chaque ligne porte un numéro de version (ou un horodatage, ou une empreinte). L’écrivain ne met à jour que si la version est toujours celle qu’il a lue, et l’incrémente. Si aucune ligne ne change, quelqu’un d’autre est passé avant, et l’application recharge et réessaie, ou prévient l’utilisateur. Cela convient aux données peu disputées et aux longs temps de réflexion : le gérant d’un restaurant qui modifie une carte dans un navigateur pendant cinq minutes ne doit pas garder un verrou de base de données tout ce temps. HTTP intègre la même idée, avec les ETags et l’en-tête If-Match.

-- Read (no lock); remember version = 12
SELECT id, name, price, version FROM menu_items WHERE id = 501;

-- Later, write only if the version is unchanged
UPDATE menu_items
   SET price = 11.00, version = version + 1
 WHERE id = 501 AND version = 12;

-- Rows affected = 1: success.
-- Rows affected = 0: a concurrent edit won; reload and retry
-- or show the user the other change.
Concurrence optimiste avec une colonne de version : la mise à jour ne réussit que si personne n’a modifié le plat de la carte depuis sa lecture.

Exemple commenté

Exemple commenté : le write skew avec deux médecins de garde

Le système de planning d’un hôpital applique une seule règle : au moins un médecin doit être de garde pour chaque créneau. Alice et Bob sont tous deux de garde ce soir. Tous deux se sentent mal et appuient sur Quitter la garde presque au même moment. Chaque demande exécute cette logique dans une transaction : compter les médecins de garde pour le créneau ; s’il y en a au moins deux, passer ma propre ligne à « pas de garde ». La base de données s’exécute en isolation par instantané (repeatable read de PostgreSQL ou serializable d’Oracle).

DébutTable doctors : Alice on_call = true, Bob on_call = true. Règle : count(on_call) >= 1.
Étape 1 : T1 (Alice) commenceT1 prend un instantané.
Étape 2 : T2 (Bob) commenceT2 prend un instantané. Les deux instantanés montrent deux médecins de garde.
Étape 3 : T1 litSELECT count(*) FROM doctors WHERE shift = 1234 AND on_call renvoie 2. La vérification passe.
Étape 4 : T2 litLa même requête dans l’instantané de T2 renvoie aussi 2. La vérification passe.
Étape 5 : T1 écritUPDATE doctors SET on_call = false WHERE name = 'Alice' AND shift = 1234
Étape 6 : T2 écritUPDATE doctors SET on_call = false WHERE name = 'Bob' AND shift = 1234. C’est une autre ligne, donc aucun conflit écriture-écriture n’est détecté.
Étape 7 : les deux validentL’isolation par instantané ne voit aucun conflit : chaque transaction a écrit une ligne que l’autre n’a pas écrite. Résultat : plus personne n’est de garde. Chaque transaction était correcte isolément ; ensemble, elles ont enfreint la règle.
En serializable (SSI)La base de données remarque que chaque transaction a lu des données que l’autre a ensuite modifiées (un cycle dangereux de dépendances lecture-écriture). L’un des commits échoue avec SQLSTATE 40001 ; la nouvelle tentative recompte, trouve un seul médecin et refuse.
Correction à un niveau plus faibleVerrouillez les lignes dont dépend la décision : SELECT name FROM doctors WHERE shift = 1234 AND on_call FOR UPDATE, puis comptez les lignes renvoyées. T2 attend désormais les verrous de T1. En read committed, T2 relit ensuite, ne trouve que Bob de garde et refuse ; en repeatable read de PostgreSQL, elle échoue plutôt avec une erreur de sérialisation, et la nouvelle tentative refuse.

Le write skew est une généralisation de la mise à jour perdue : les transactions lisent les mêmes données mais écrivent des lignes différentes, si bien que la règle de l’isolation par instantané selon laquelle le premier qui valide l’emporte ne se déclenche jamais.

Le même motif apparaît dans les systèmes de réservation (deux personnes qui réservent la dernière place ou des créneaux de réunion qui se chevauchent), les noms d’utilisateur (deux inscriptions qui vérifient qu’un nom est libre) et la banque (deux retraits sur des comptes différents qui, ensemble, dépassent un découvert autorisé commun).

Lorsque les lignes qui prouveraient un conflit n’existent pas encore, comme pour deux réservations qui se chevauchent, FOR UPDATE n’a rien à verrouiller. Les options sont l’isolation sérialisable, une contrainte d’unicité ou d’exclusion (PostgreSQL peut interdire de manière déclarative les plages horaires qui se chevauchent), ou la matérialisation du conflit : créer des lignes, par exemple une par salle et par créneau, uniquement pour pouvoir les verrouiller.

Choisir une stratégie d’isolation

Il n’existe pas un seul bon niveau ; il existe un bon niveau pour chaque élément de logique. Une approche pratique part des invariants, pas des paramètres.

Liste de contrôle : sécuriser un chemin de code transactionnel

  • Nommez l’invariantÉcrivez la règle que le code protège (solde jamais négatif, un seul livreur par commande, au moins un médecin de garde). S’il n’y en a pas, le niveau par défaut convient probablement.
  • Préférez les contraintes et les instructions atomiquesLes index uniques, les clés étrangères, les contraintes CHECK et d’exclusion, ainsi que les mises à jour conditionnelles en une seule instruction, sont appliqués par la base de données quel que soit le niveau d’isolation.
  • Repérez les lectures-modifications-écrituresTout code qui lit une valeur, décide dans l’application et écrit en retour présente un risque de mise à jour perdue ou de write skew. Protégez-le avec FOR UPDATE, une colonne de version ou l’isolation sérialisable.
  • Connaissez la sémantique réelle de votre base de donnéesVérifiez ce que signifient réellement les noms de niveaux de votre produit (repeatable read diffère entre PostgreSQL et MySQL ; le serializable d’Oracle est de l’isolation par instantané).
  • Réessayez correctementSerializable et repeatable read peuvent abandonner une transaction sur une erreur de sérialisation ou un interblocage. Relancez toute la transaction avec un délai croissant entre les tentatives (back-off), et gardez les effets de bord externes en dehors d’elle.
  • Gardez les transactions courtesPas de temps de réflexion de l’utilisateur, d’appels réseau ni de gros rapports dans une transaction ; les transactions longues conservent des verrous et font gonfler le stockage MVCC.
  • Testez en situation de concurrenceExécutez les chemins critiques en parallèle dans un test d’intégration sur le vrai moteur de base de données, pas sur un substitut en mémoire à la sémantique différente.

En règle générale, read committed avec verrouillage explicite ou vérifications de version est un choix par défaut solide et rapide pour la plupart des applications métier, à condition que l’équipe repère avec rigueur les chemins lecture-modification-écriture. Serializable est le choix par défaut le plus sûr quand les règles sont complexes, que l’équipe est grande ou que la violation d’un invariant coûte cher (argent, médecine, sûreté), et de nombreuses équipes l’utilisent aujourd’hui pour les chemins critiques, en acceptant les nouvelles tentatives. Les praticiens sont réellement en désaccord sur ce point : certains soutiennent que serializable par défaut évite toute une classe de bugs que la revue de code ne détectera jamais ; d’autres estiment que ses nouvelles tentatives et sa contention coûtent trop cher et que le verrouillage explicite rend l’intention visible. Les deux positions se défendent si le choix est délibéré et testé.

Vérifiez votre compréhension

Pourquoi l’isolation par instantané n’est-elle pas sérialisable ?

Elle permet le write skew : deux transactions peuvent lire des données qui se recoupent et écrire des lignes différentes, enfreignant ensemble une règle que chacune avait vérifiée, sans qu’aucun conflit écriture-écriture soit détecté.

De quoi dépend le C d’ACID ?

Surtout de l’application : la base de données applique les contraintes déclarées, mais les invariants métier ne sont respectés que si les transactions de l’application les préservent.

Quand choisiriez-vous le contrôle de concurrence optimiste plutôt que pessimiste ?

Quand les conflits sont rares ou que le temps de réflexion entre la lecture et l’écriture est long, comme lorsqu’un utilisateur modifie un formulaire, si bien que garder des verrous serait coûteux ou impossible.

Pourquoi un code qui utilise l’isolation sérialisable doit-il pouvoir être relancé sans risque ?

Les transactions sérialisables peuvent être abandonnées sur des échecs de sérialisation ou des interblocages, et la bonne réponse est de réexécuter toute la transaction : elle ne doit donc avoir aucun effet de bord externe.

Suivant · 7.4Réplication et partitionnementComment les bases de données copient les données pour la disponibilité et le passage à l’échelle des lectures, ce que le retard de réplication et le failover font à l’exactitude, et comment le partitionnement par plage ou par hachage, le hachage cohérent et les index secondaires façonnent une conception partitionnée (sharding).Continuer →

Sources et lectures complémentaires

Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.

Demander : « Notre système de réservation attribue parfois deux fois la même table alors que nous vérifions d’abord la disponibilité. Quel niveau d’isolation ou quelle approche de verrouillage devrions-nous utiliser ? »