8 min

Comment les garanties ACID façonnent des systèmes transactionnels fiables

Comprenez comment les garanties ACID influent sur la conception des bases de données et le comportement des applications. Explorez atomicité, cohérence, niveaux d'isolation, durabilité, compromis et exemples concrets.

Comment les garanties ACID façonnent des systèmes transactionnels fiables

Ce que « ACID » signifie pour les transactions quotidiennes

Quand vous payez vos courses, réservez un vol ou effectuez un virement, vous attendez un résultat sans équivoque : soit ça a marché, soit ça n'a pas marché. Les bases de données cherchent à offrir la même certitude — même lorsque de nombreuses personnes utilisent le système en même temps, que des serveurs plantent ou que le réseau flanche.

Une transaction, en termes simples

Une transaction est une unité de travail que la base traite comme un seul « paquet ». Elle peut inclure plusieurs étapes — soustraire de l'inventaire, créer une commande, débiter une carte et écrire un reçu — mais elle doit se comporter comme une action cohérente.

Si une étape échoue, le système doit revenir à un point sûr plutôt que de laisser un travail à moitié fini.

Pourquoi les mises à jour partielles posent de vrais problèmes business

Les mises à jour partielles ne sont pas de simples bugs techniques ; elles deviennent des tickets au support et un risque financier. Par exemple :

  • Un paiement est prélevé, mais la commande n'est pas créée — le client est débité sans confirmation.
  • Une commande est créée, mais l'inventaire n'est pas réduit — votre site survend et vous annulez ensuite.
  • Un virement débite un compte mais ne crédite pas l'autre — les soldes ne correspondent plus.

Ces échecs sont difficiles à diagnostiquer parce que tout semble « en grande partie correct », mais les chiffres ne collent pas.

ACID est un ensemble de garanties (pas un produit)

ACID est l'abréviation de quatre garanties que beaucoup de bases de données peuvent fournir pour les transactions :

  • Atomicité : exécution tout-ou-rien
  • Cohérence : les données restent conformes aux règles valides
  • Isolation : les transactions concurrentes ne s'interfèrent pas de façon non sécurisée
  • Durabilité : une fois commitée, une modification persiste

Ce n'est pas une marque de base de données ni un seul réglage à activer ; c'est une promesse de comportement.

Bénéfices — et coûts à prévoir

Des garanties plus fortes signifient généralement que la base doit faire plus de travail : coordination supplémentaire, attente de verrous, suivi de versions et écriture dans des journaux. Cela peut réduire le débit ou augmenter la latence sous forte charge. L'objectif n'est pas « le maximum d'ACID en permanence », mais de choisir les garanties qui correspondent à vos risques métier réels.

Atomicité : mises à jour tout-ou-rien

L'atomicité signifie qu'une transaction est traitée comme une unité de travail : elle se termine complètement ou n'a aucun effet. Vous n'obtenez jamais une « mise à jour à moitié visible » dans la base.

Un exemple simple de virement bancaire

Imaginez transférer 50 $ d'Alice à Bob. En coulisses, cela implique généralement au moins deux changements :

  • Soustraire 50 $ du solde d'Alice
  • Ajouter 50 $ au solde de Bob

Avec l'atomicité, ces deux changements réussissent ensemble ou échouent ensemble. Si le système ne peut pas faire les deux en toute sécurité, il ne doit en faire aucun. Cela évite le cauchemar où Alice est débitée mais Bob ne reçoit pas l'argent (ou l'inverse).

Commit vs rollback (en langage courant)

Les bases de données offrent deux sorties pour une transaction :

  • Commit : « toutes les étapes ont réussi ; officialisez les résultats. »
  • Rollback : « quelque chose a mal tourné ; annulez tout ce qui a été fait par cette transaction. »

Un modèle mental utile est « brouillon vs publication ». Tant que la transaction s'exécute, les changements sont provisoires. Seul un commit les publie.

Que peut-il se passer en plein milieu d'une transaction ?

L'atomicité est cruciale parce que les pannes sont normales :

  • Crash de l'app : votre service s'arrête après avoir mis à jour une table mais avant la suivante.
  • Perte réseau : l'app ne peut pas joindre la base, ou le client ne reçoit jamais la réponse de succès.
  • Coupure de courant : le serveur de base plante subitement.

Si l'un de ces événements survient avant la fin du commit, l'atomicité permet à la base de faire un rollback pour que le travail partiel ne s'infiltre pas dans les soldes réels.

Atomicité plus idempotence et reprises

L'atomicité protège l'état de la base, mais votre application doit quand même gérer l'incertitude — surtout quand une perte réseau rend incertain le fait qu'un commit ait eu lieu.

Deux compléments pratiques :

  • Reprises (retries) : répéter une requête quand vous n'obtenez pas de réponse.
  • Idempotence : rendre la répétition d'une même requête sans danger (par exemple, utiliser une clé d'idempotence pour que « virement #123 » ne soit appliqué au plus qu'une fois).

Ensemble, transactions atomiques et reprises idempotentes vous aident à éviter à la fois les mises à jour partielles et les doubles prélèvements accidentels.

Cohérence : garder les données dans des règles valides

La cohérence dans ACID ne signifie pas « les données ont l'air raisonnables » ou « toutes les réplicas correspondent ». Cela signifie que chaque transaction doit faire passer la base d'un état valide à un autre état valide — selon les règles que vous définissez.

La cohérence est définie par des règles que vous choisissez

Une base de données ne peut assurer la cohérence que par rapport à des contraintes explicites, des triggers et des invariants qui décrivent ce que « valide » signifie pour votre système. ACID n'invente pas ces règles ; il les applique pendant les transactions.

Exemples courants :

  • Clés étrangères : chaque order.customer_id doit pointer vers un client existant.
  • Contraintes d'unicité : deux utilisateurs ne peuvent pas partager le même e‑mail.
  • Check constraints / invariants : un solde ne peut pas être négatif, ou une quantité d'article ne peut pas être inférieure à zéro.

Si ces règles sont en place, la base rejettera toute transaction qui les violerait — vous n'obtiendrez donc pas de données « à moitié valides ».

Validation côté application vs. contraintes en base

La validation dans l'app est importante, mais pas suffisante :

  • Validation côté app améliore l'UX (messages d'erreur clairs, retours précoces) et peut appliquer des règles métier complexes.
  • Contraintes en base servent de dernier rempart — surtout quand plusieurs services, jobs en arrière-plan, imports ou outils admin écrivent dans les mêmes tables.

Un mode de défaillance classique : vérifier dans l'app que l'email est disponible, puis insérer la ligne. Sous concurrence, deux requêtes peuvent réussir la vérification simultanément. Une contrainte d'unicité en base garantit qu'une seule insertion réussira.

À quoi ressemble la cohérence en pratique

Si vous encodez « pas de soldes négatifs » comme contrainte (ou l'appliquez de façon fiable dans une seule transaction), alors tout transfert qui mettrait le compte dans le rouge doit échouer dans son ensemble. Si vous n'encodiez cette règle nulle part, ACID ne peut pas la protéger — car il n'y a rien à faire appliquer.

La cohérence revient finalement à être explicite : définissez les règles, puis laissez les transactions garantir qu'elles ne sont jamais brisées.

Isolation : travailler en sécurité sous concurrence

L'isolation garantit que les transactions ne se marchent pas sur les pieds. Pendant qu'une transaction est en cours, les autres ne devraient pas voir de travail à moitié fini ni l'écraser par inadvertance. L'objectif est simple : chaque transaction doit se comporter comme si elle tournait seule, même quand de nombreux utilisateurs sont actifs en même temps.

Pourquoi la concurrence complique les choses

Les systèmes réels sont chargés : des clients passent des commandes, des agents support modifient des profils, des jobs en arrière-plan rapprochent des paiements — le tout en même temps. Ces actions se chevauchent et touchent souvent les mêmes lignes (un solde, un compteur d'inventaire, un créneau de réservation).

Sans isolation, le timing devient une partie de votre logique métier. Une mise à jour « soustraire du stock » peut faire course avec un autre paiement, ou un rapport peut lire des données en plein changement et afficher des chiffres qui n'ont jamais existé dans un état stable.

L'isolation est généralement configurable

L'isolation « totale » (se comporter comme si vous étiez seul) peut être coûteuse. Elle peut réduire le débit, augmenter l'attente (verrous) ou provoquer des reprises de transactions. Parallèlement, de nombreux workflows n'ont pas besoin de la protection la plus stricte — la lecture des analytics d'hier, par exemple, peut tolérer des incohérences mineures.

C'est pourquoi les bases offrent des niveaux d'isolation configurables : vous choisissez le risque de concurrence acceptable en échange d'une meilleure performance et de moins de conflits.

Aperçu rapide : anomalies que l'isolation prévient (ou permet)

Quand l'isolation est trop faible pour votre charge, vous rencontrerez des anomalies classiques :

  • Lectures sales : lire des changements qu'une autre transaction n'a pas encore commités.
  • Lost updates : deux transactions s'écrasent l'une l'autre et un ensemble de changements disparaît.
  • Phantom reads : relancer une requête retourne un ensemble de lignes différent parce qu'une autre transaction a inséré ou supprimé des lignes correspondantes.

Comprendre ces modes d'échec facilite le choix d'un niveau d'isolation qui corresponde aux promesses de votre produit.

Anomalies courantes que l'isolation prévient (ou permet)

Expérimentez sans crainte
Itérez sur les schémas et les niveaux d'isolation, puis créez des instantanés et restaurez si besoin.

L'isolation détermine ce que vous êtes autorisé à « voir » pendant que d'autres transactions s'exécutent. Quand elle est trop faible, vous pouvez obtenir des anomalies — des comportements techniquement possibles mais surprenants pour les utilisateurs.

Anomalies de lecture

Une lecture sale se produit quand vous lisez des données écrites par une autre transaction mais non commitée.

Scénario : Alex transfère 500 $ hors d'un compte, le solde devient temporairement 200 $, et vous lisez ces 200 $ avant que le transfert d'Alex n'échoue et soit annulé.

Conséquence utilisateur : un client voit un solde incorrectement bas, une règle antifraude se déclenche à tort, ou un agent support donne une mauvaise réponse.

Une lecture non répétable signifie que vous lisez deux fois la même ligne et obtenez des valeurs différentes parce qu'une autre transaction a commit entre-temps.

Scénario : vous chargez le total d'une commande (49,00 $), puis rafraîchissez et voyez 54,00 $ parce qu'une ligne de remise a été supprimée.

Conséquence utilisateur : « mon total a changé pendant que je finalisais ma commande », ce qui mène à de la défiance ou à l'abandon du panier.

Un phantom read ressemble à une lecture non répétable, mais sur un ensemble de lignes : une seconde requête retourne des lignes en plus (ou en moins) car une autre transaction a inséré/supprimé des enregistrements correspondants.

Scénario : une recherche d'hôtel affiche « 3 chambres disponibles », puis lors de la réservation le système re-vérifie et n'en trouve aucune parce que de nouvelles réservations ont été ajoutées.

Conséquence utilisateur : tentatives de double réservation, écrans de disponibilité incohérents ou survente d'inventaire.

Anomalies d'écriture (bugs fréquents en pratique)

Un lost update survient lorsque deux transactions lisent la même valeur et écrivent des mises à jour ; la dernière écriture écrase la précédente.

Scénario : deux admins modifient le même prix produit. Tous deux partent de 10 $ ; l'un enregistre 12 $, l'autre 11 $ en dernier.

Conséquence utilisateur : la modification de quelqu'un disparaît ; totaux et rapports erronés.

Un write skew se produit lorsque deux transactions effectuent chacune un changement valide individuellement, mais ensemble elles violent une règle.

Scénario : règle : « au moins un médecin de garde doit être planifié ». Deux médecins se déclarent chacun indisponibles après avoir vérifié que l'autre est toujours de garde.

Conséquence utilisateur : se retrouver sans couverture, malgré des transactions qui « passent » individuellement.

Pourquoi ne pas toujours utiliser l'isolation la plus stricte ?

Une isolation plus forte réduit les anomalies mais peut augmenter l'attente, les reprises et les coûts sous haute concurrence. Beaucoup de systèmes choisissent une isolation plus faible pour les lectures analytiques, tout en utilisant des réglages plus stricts pour les transferts d'argent, les réservations et autres flux critiques pour la cohérence.

Niveaux d'isolation : choisir le bon réglage de sécurité

L'isolation traite de ce que votre transaction est autorisée à « voir » pendant que d'autres s'exécutent. Les bases l'exposent via des niveaux d'isolation : les niveaux supérieurs réduisent les comportements surprenants, mais coûtent en débit ou en attentes.

Les niveaux courants

  • Read Uncommitted : vous pouvez lire des changements non commités par une autre transaction (lectures sales). Presque rien n'est empêché.
  • Read Committed : vous lisez seulement des données commités, donc les lectures sales sont empêchées. Mais si vous exécutez la même requête deux fois, vous pouvez voir des résultats différents car quelqu'un a commit entre-temps (lectures non répétables).
  • Repeatable Read : les lectures déjà faites restent stables pendant la transaction, donc les lectures non répétables sont généralement empêchées. Selon le moteur, vous pouvez encore voir des « phantoms » ou pas.
  • Serializable : les transactions se comportent comme si elles s'exécutaient une par une. C'est le réglage le plus fort, empêchant généralement les lectures sales, les lectures non répétables et les phantoms, et réduisant de nombreuses anomalies subtiles d'écriture.

Choisir un niveau : débit vs. exactitude

Les équipes choisissent souvent Read Committed par défaut pour les apps orientées utilisateur : bonne performance et l'absence de lectures sales correspond à la plupart des attentes.

Utilisez Repeatable Read quand vous avez besoin de résultats stables dans une transaction (par ex. générer une facture) et que vous pouvez tolérer un certain coût. Utilisez Serializable quand l'exactitude prime sur la concurrence (par ex. empêcher la survente), ou quand il est difficile de raisonner sur les courses dans le code applicatif.

Read Uncommitted est rare en OLTP ; il sert parfois pour du monitoring ou des rapports approximatifs où des lectures erronées occasionnelles sont acceptables.

Avertissement important : le comportement varie

Les noms sont standardisés, mais les garanties exactes diffèrent selon le moteur de base de données (et parfois selon la configuration). Consultez la documentation de votre SGBD et testez les anomalies qui importent pour votre business.

Durabilité : faire tenir les commits

La durabilité signifie que, une fois qu'une transaction est committée, ses résultats doivent survivre à un crash — coupure de courant, redémarrage de processus ou reboot inattendu de la machine. Si votre app dit à un client « paiement réussi », la durabilité garantit que la base ne va pas « oublier » ce fait après la prochaine panne.

Comment les bases rendent les commits persistants

La plupart des bases relationnelles atteignent la durabilité via le write-ahead logging (WAL). De façon simplifiée, la base écrit un « reçu » séquentiel des changements dans un journal sur disque avant de considérer la transaction comme committée. Si la base plante, elle peut rejouer le journal au démarrage pour restaurer les changements commités.

Pour garder les temps de récupération raisonnables, les bases créent aussi des checkpoints. Un checkpoint est un moment où la base s'assure qu'assez de changements récents sont écrits dans les fichiers de données principaux, pour que la récupération n'ait pas à rejouer un historique de journal infini.

La durabilité dépend du stockage et de la configuration

La durabilité n'est pas un interrupteur on/off ; elle dépend de l'agressivité avec laquelle la base force les données vers un stockage stable :

  • Avec des réglages synchrones, la base attend que le journal soit flushé (souvent via un fsync du système) avant de confirmer le commit. C'est plus sûr, mais peut ajouter de la latence.
  • Avec des réglages asynchrones, la base peut ack le commit avant que le journal soit totalement sur un stockage durable. La performance peut s'améliorer, mais un crash peut faire perdre les transactions « committées » les plus récentes.

Le matériel sous-jacent compte aussi : SSD, contrôleurs RAID avec cache d'écriture et volumes cloud peuvent se comporter différemment en cas de défaillance.

Sauvegardes et réplication sont liés — mais différents

Les sauvegardes et la réplication vous aident à récupérer ou réduire le downtime, mais elles ne sont pas identiques à la durabilité. Une transaction peut être durable sur le primaire même si elle n'a pas encore atteint une réplique, et les sauvegardes sont typiquement des snapshots à un instant donné plutôt que des garanties commit-par-commit.

Comment les bases appliquent ACID en coulisses

Testez la concurrence dès le début
Modélisez rapidement des interblocages, réessais et transactions courtes dans un projet fonctionnel que vous pouvez exécuter.

Quand vous BEGIN une transaction puis COMMIT, la base coordonne de nombreux éléments : qui peut lire quelles lignes, qui peut les mettre à jour, et ce qui se passe si deux personnes essaient de modifier le même enregistrement en même temps.

Contrôle de concurrence pessimiste vs optimiste

Un choix clé est la manière de gérer les conflits :

  • Verrouillage pessimiste suppose que les conflits sont probables. Quand une transaction met à jour une ligne, la base verrouille cette ligne pour que les autres transactions attendent. Cela évite beaucoup d'anomalies, mais peut causer du blocage.
  • Approches optimistes supposent que les conflits sont rares. Les transactions avancent avec moins de blocage, et la base détecte les conflits au commit (ou via des checks) et peut rejeter une transaction pour qu'elle soit relancée.

Beaucoup de systèmes mélangent les deux selon la charge et le niveau d'isolation.

MVCC : les lecteurs n'empêchent pas les écrivains

Les bases modernes utilisent souvent MVCC (Multi-Version Concurrency Control) : plutôt que de garder une seule copie d'une ligne, la base conserve plusieurs versions.

  • Les lecteurs voient un snapshot cohérent (une version plus ancienne) sans attendre.
  • Les écrivains créent une nouvelle version pendant que les lectures continuent.

C'est une grande raison pour laquelle certaines bases gèrent beaucoup de lectures et d'écritures en parallèle avec moins de blocage — bien que les conflits écriture/écriture nécessitent toujours une résolution.

Deadlocks : quand l'attente forme une boucle

Les verrous peuvent mener à des deadlocks : la transaction A attend un verrou détenu par B, tandis que B attend un verrou détenu par A.

Les bases détectent typiquement le cycle et abortent une transaction (la « victime » du deadlock), renvoyant une erreur pour que l'application puisse réessayer.

Signes pratiques que quelque chose cloche

Si l'application subit des frottements liés à l'application d'ACID, vous verrez souvent :

  • Augmentation des waits de verrous pendant les pics
  • Timeouts (requêtes échouant après trop d'attente)
  • Points chauds de contention (quelques lignes/tables mises à jour constamment, par ex. compteurs ou champs « last seen »)

Ces symptômes indiquent souvent qu'il est temps de revoir la taille des transactions, l'indexation ou la stratégie d'isolation/verrouillage adaptée à la charge.

Comment ACID influence la conception applicative

Les garanties ACID ne sont pas que de la théorie ; elles influencent la conception des API, des jobs en arrière-plan et même des flux UI. L'idée centrale est simple : décidez quelles étapes doivent réussir ensemble, puis encapsulez uniquement ces étapes dans une transaction.

Concevoir des API autour d'« un changement métier »

Une API transactionnelle bien conçue mappe généralement à une seule action métier, même si elle touche plusieurs tables. Par exemple, une opération /checkout pourrait : créer une commande, réserver l'inventaire et enregistrer une intention de paiement. Ces écritures en base devraient généralement vivre dans une même transaction pour qu'elles commitent ensemble (ou rollbackent ensemble) si une validation échoue.

Un schéma courant :

  • Valider les entrées avant d'ouvrir la transaction.
  • Ouvrir une transaction.
  • Faire le minimum de lectures/écritures nécessaires.
  • Commit.

Cela conserve atomicité et cohérence tout en évitant des transactions longues et fragiles.

Bornes transactionnelles dans requêtes, services et jobs

Où vous placez les limites de transaction dépend de ce que signifie « une unité de travail » :

  • Requêtes utilisateur : gardez les transactions courtes — idéalement quelques requêtes. Ne conservez pas de verrous pendant le rendu de vues ou en attendant des appels externes.
  • Jobs en arrière-plan : traitez chaque tentative de job comme une unité de travail. Si un job traite 10 000 enregistrements, committez par batchs pour pouvoir redémarrer en toute sécurité.
  • Bornes de services : préférez garder une transaction à l'intérieur d'une base d'un seul service. Traverser des services nécessite souvent une autre approche (par ex. outbox), car une transaction ACID ne couvre pas facilement plusieurs bases.

Gestion des erreurs : rollback, retries et replays sûrs

ACID aide, mais l'app doit toujours gérer les erreurs correctement :

  • Rollback en cas d'erreur : si une étape échoue, annulez la transaction pour que les mises à jour partielles ne fuient pas.
  • Retry pour erreurs transitoires : échecs de sérialisation et deadlocks sont normaux sous concurrence. Retenter la transaction complète est souvent la bonne solution.
  • Rendre les opérations idempotentes : si une requête est rejouée (par le client ou le runner de jobs), vous devez pouvoir la rejouer sans double-prélèvement ni double-expédition — utilisez des clés d'idempotence et des contraintes uniques.

Anti-patterns courants

Évitez les transactions longues, appeler des API externes dans une transaction et le temps de réflexion de l'utilisateur dans une transaction (par ex. « verrouiller la ligne du panier, demander la confirmation à l'utilisateur »). Ces pratiques augmentent la contention et rendent les conflits d'isolation bien plus probables.

Où les outils peuvent aider (sans changer les fondamentaux)

Si vous construisez un système transactionnel rapidement, le risque majeur n'est généralement pas « ne pas connaître ACID » — c'est disperser accidentellement une action métier sur plusieurs endpoints, jobs ou tables sans frontière transactionnelle claire.

Des plateformes comme Koder.ai peuvent vous aider à aller plus vite tout en concevant autour d'ACID : vous pouvez décrire un workflow (par ex. « checkout avec réservation d'inventaire et intention de paiement ») dans un chat orienté planning, générer une UI React plus un backend Go + PostgreSQL, et itérer avec snapshots/rollback si un schéma ou une frontière transactionnelle doit changer. La base continue d'appliquer les garanties ; la valeur additionnelle est d'accélérer le chemin d'une conception correcte à une implémentation fonctionnelle.

ACID dans les systèmes distribués et multiservices

Rendez les réessais sûrs
Créez plus rapidement des endpoints idempotents en générant des modèles d'API que vous pouvez affiner et tester.

Une base unique peut généralement livrer des garanties ACID à l'intérieur d'une même transaction. Une fois que vous répartissez le travail sur plusieurs services (et souvent plusieurs bases), ces mêmes garanties deviennent plus difficiles à maintenir — et plus coûteuses si vous essayez.

Cohérence vs disponibilité : le compromis que vous ressentez en production

La cohérence stricte signifie que chaque lecture voit la « dernière vérité commitée ». La haute disponibilité signifie que le système continue de répondre même quand des parties sont lentes ou inaccessibles.

Dans une architecture multiservice, un problème réseau temporaire peut forcer un choix : bloquer ou échouer les requêtes jusqu'à ce que tous les participants s'accordent (plus cohérent, moins disponible), ou accepter que des services soient brièvement en décalage (plus disponible, moins cohérent). Rien n'est « toujours juste » — tout dépend des erreurs que votre métier peut tolérer.

Pourquoi les transactions distribuées sont difficiles

Les transactions distribuées exigent une coordination au-delà de frontières que vous ne contrôlez pas complètement : latences réseau, retries, timeouts, crashs de services et pannes partielles.

Même si chaque service est correct, le réseau peut créer de l'ambiguïté : le service de paiement a‑t‑il commit tandis que le service de commande n'a jamais reçu l'accusé ? Pour résoudre cela en toute sécurité, on utilise des protocoles de coordination (comme two‑phase commit), qui peuvent être lents, réduire la disponibilité en cas de panne et ajouter de la complexité opérationnelle.

Patterns pratiques qui remplacent « une grosse transaction »

Sagas découpent un workflow en étapes, chacune commitée localement. Si une étape ultérieure échoue, les étapes antérieures sont « annulées » par des actions compensatoires (par ex. rembourser une charge).

Pattern outbox/inbox rend la publication d'événements et leur consommation fiable. Un service écrit les données métier et un enregistrement « événement à publier » dans la même transaction locale (outbox). Les consommateurs enregistrent les IDs de message traités (inbox) pour gérer les retries sans dupliquer les effets.

Cohérence éventuelle accepte de courts décalages entre services, avec un plan clair de réconciliation.

Quand relaxer les garanties — et comment contrôler le risque

Relaxez les garanties lorsque :

  • Vous pouvez tolérer des divergences temporaires (statut d'expédition retardé par rapport à la création de commande).
  • Vous pouvez corriger les erreurs par compensations (remboursements, annulations).
  • La latence et la disponibilité importent plus que la cohérence globale instantanée.

Contrôlez le risque en définissant des invariants (ce qui ne doit jamais être violé), en concevant des opérations idempotentes, en utilisant timeouts et retries avec backoff, et en surveillant la dérive (sagas bloquées, compensations répétées, tables outbox qui gonflent). Pour des invariants vraiment critiques (par ex. « ne jamais dépasser un compte »), conservez-les au sein d'un seul service et d'une seule transaction de base si possible.

Checklist pratique : concevoir, tester et surveiller des systèmes ACID

Une transaction peut être « correcte » en test unitaire et quand même échouer sous trafic réel, redémarrages et concurrence. Utilisez cette checklist pour aligner les garanties ACID sur le comportement en production.

1) Conception : définir les invariants et la frontière transactionnelle

Commencez par écrire ce qui doit toujours être vrai (vos invariants de données). Exemples : « le solde ne devient jamais négatif », « le total d'une commande égale la somme des lignes », « l'inventaire ne peut pas passer sous zéro », « un paiement est lié à exactement une commande ». Traitez-les comme des règles produit, pas de simples détails de base.

Puis décidez ce qui doit être à l'intérieur d'une transaction versus ce qui peut être différé.

  • Invariants de données : listez les tables/lignes impliquées et la règle exacte.
  • Scénarios de panne : crash de process en plein request, timeout réseau après commit, reprise entraînant des doublons, basculement de réplica, disque plein.
  • Profil de concurrence : quelles opérations s'exécutent en parallèle (pics de checkout, mises à jour en batch, jobs planifiés), points chauds attendus, et si les lectures doivent être « à l'instant » ou peuvent être légèrement périmées.

Gardez les transactions petites : touchez moins de lignes, faites moins de travail (pas d'appels externes), et commitez rapidement.

2) Test : prouver le comportement sous courses et pannes

Faites de la concurrence une dimension de test à part entière.

  • Tests de condition de course : exécutez la même opération critique en parallèle (par ex. deux checkouts pour le dernier article) et assertz que les invariants ne sont jamais brisés.
  • Injection de pannes : tuez le process app en plein milieu d'une transaction ; injectez des timeouts ; simulez des retries ; forcez un redémarrage de DB ; vérifiez que les résultats sont soit commités une fois, soit correctement rollbackés.
  • Tests de charge avec vérifications d'exactitude : sous pic de charge, validez non seulement la latence mais aussi les totaux, comptages et l'absence de doublons.

Si vous supportez les retries, ajoutez une clé d'idempotence explicite et testez « requête répétée après succès ».

3) Surveillance : attraper les douleurs ACID avant les utilisateurs

Surveillez des indicateurs montrant que vos garanties deviennent coûteuses ou fragiles :

  • Temps d'attente de verrous et files d'attente (conten­tion croissante)
  • Deadlocks (fréquence, requêtes victimes)
  • Transactions longues (souvent cause racine)
  • Lag de réplication (lectures périmées et basculement retardé)
  • Temps de commit/fsync (pression sur le stockage ; coût de durabilité)

Alertez sur les tendances, pas seulement sur les pics, et reliez les métriques aux endpoints ou jobs qui les causent.

Règles pratiques : isolation et portée des transactions

Utilisez l'isolation la plus faible qui protège encore vos invariants ; n'augmentez pas par défaut au maximum. Quand vous avez besoin d'exactitude stricte pour une petite section critique (mouvement d'argent, décrément d'inventaire), restreignez la transaction à juste cette section et placez le reste en dehors.

FAQ

Que signifie ACID dans une base de données, en termes pratiques ?

ACID est un ensemble de garanties transactionnelles qui aident les bases de données à se comporter de façon prévisible en cas de pannes et de concurrence :

  • Atomicité : toutes les étapes réussissent ou aucune
  • Cohérence : chaque commit préserve vos règles/contraintes définies
  • Isolation : les transactions concurrentes n'entraînent pas d'interférences dangereuses
  • Durabilité : les changements commités survivent aux plantages
Qu'est-ce qu'une transaction, et pourquoi est-ce important ?

Une transaction est une « unité de travail » que la base de données traite comme un seul paquet. Même si elle exécute plusieurs instructions SQL (par exemple : créer une commande, décrémenter l'inventaire, enregistrer une intention de paiement), elle n'a que deux issues :

  • Commit : toutes les modifications deviennent officielles
  • Rollback : aucune des modifications n'est appliquée
Pourquoi les mises à jour partielles posent-elles un si gros problème business ?

Parce que les mises à jour partielles créent des contradictions concrètes et coûteuses à corriger, par exemple :

  • client débité mais commande non enregistrée
  • commande enregistrée mais inventaire non réduit (survente)
  • un côté d'un virement appliqué mais pas l'autre

ACID (notamment atomicité + cohérence) empêche que ces états « à moitié terminés » deviennent la vérité visible.

Comment l'atomicité empêche-t-elle les opérations à moitié terminées ?

L'atomicité garantit que la base de données n'expose jamais une transaction « à moitié terminée ». Si quelque chose échoue avant le commit — crash de l'app, perte réseau, redémarrage de la BDD — la transaction est annulée pour que les étapes précédentes ne se propagent pas dans l'état persistant.

En pratique, l'atomicité rend sûres les opérations multi-étapes (par exemple un virement qui met à jour deux soldes).

Si ACID est sûr, pourquoi ai-je encore besoin d'idempotence et de reprises ?

On ne peut pas toujours savoir si un commit a eu lieu si le client perd la réponse (par exemple timeout réseau juste après le commit). Combinez les transactions ACID avec :

  • Retries pour les erreurs transitoires
  • Clés d'idempotence (ou contraintes uniques) pour que la répétition d'une même requête soit appliquée au plus une fois

Cela évite à la fois les mises à jour partielles et les doubles prélèvements/écritures accidentels.

Que signifie « cohérence » dans ACID (et que cela ne signifie pas) ?

Dans ACID, « cohérence » signifie que la base de données passe d'un état valide à un autre état valide selon des règles que vous définissez — contraintes, clés étrangères, unicité, checks.

Si vous n'encodiez pas une règle (par ex. « le solde ne peut pas être négatif »), ACID ne pourra pas l'appliquer automatiquement. La base a besoin d'invariants explicites pour protéger cette propriété.

Pourquoi utiliser des contraintes en base si mon application valide déjà les entrées ?

La validation côté application améliore l'expérience utilisateur et peut appliquer des règles métier complexes, mais elle peut échouer sous concurrence (deux requêtes peuvent valider la même condition en même temps).

Les contraintes en base agissent comme le dernier rempart :

  • Contraintes d'unicité empêchent les emails dupliqués
  • Clés étrangères empêchent les enregistrements orphelins
  • Check constraints/invariants empêchent les valeurs invalides

Utilisez les deux : validez tôt dans l'app, faites respecter définitivement en base.

Quels types de bugs de concurrence l'isolation protège-t-elle ?

L'isolation contrôle ce que votre transaction peut observer pendant que d'autres s'exécutent. Une isolation faible peut produire des anomalies telles que :

  • Lectures sales (dirty reads) : voir des données non commités
  • Lectures non répétables : la même ligne change entre deux lectures
  • Phantoms : un ensemble de lignes diffère entre deux requêtes
  • Lost updates / write skew : des écritures concurrentes rompent la cohérence

Les niveaux d'isolation vous permettent d'équilibrer performance et protection contre ces anomalies.

Comment choisir un niveau d'isolation sans tuer les performances ?

Une base pratique par défaut est Read Committed pour beaucoup d'applications OLTP : empêche les lectures sales et offre de bonnes performances. Montez en niveau quand c'est nécessaire :

  • Repeatable Read pour des lectures stables dans une transaction (ex. génération de facture)
  • Serializable pour des invariants critiques (ex. éviter la survente) si vous pouvez tolérer plus de contention/reprises

Confirmez toujours le comportement dans votre engine : les détails varient.

Que garantit la durabilité, et qu'est-ce qui peut l'affaiblir ?

La durabilité garantit que, une fois qu'une base de données confirme un commit, le changement survivra aux plantages. C'est généralement implémenté via le write-ahead logging (WAL) et des checkpoints.

Ce qui peut l'affaiblir :

  • paramètres asynchrones qui ack avant l'écriture durable (plus rapide, mais risque de perte)
  • matériel (contrôleurs RAID avec cache qui n'est pas protégé, volumes cloud) qui se comporte différemment en panne

Les backups et la réplication aident la récupération et la disponibilité, mais ne sont pas identiques à la garantie de durabilité.

Related posts