Read in English · Leer en español · Auf Deutsch lesen
Section 6.7 sur 6.8 · Avancé · 30 min de lecture
Transactions distribuées, sagas et patron outbox
Leçon vidéo
5 min · narré en anglais, avec sous-titresÉcouter seulement (audio)
Lire la transcription
Quand une action métier s’étend sur plusieurs services, aucune transaction unique ne peut la couvrir. Dans cette leçon, vous allez découvrir pourquoi on évite la validation en deux phases entre services, comment les sagas permettent malgré tout de garder des processus corrects, et comment envoyer des messages de façon fiable.
Réserver des vacances, c’est réserver un vol, un hôtel et une voiture de location, auprès de trois entreprises différentes. Personne ne peut faire en sorte que les trois se produisent exactement au même instant. Vous les réservez donc l’une après l’autre. Si la voiture de location s’avère indisponible, vous annulez l’hôtel et le vol, en payant peut-être des frais d’annulation. C’est une saga : des étapes distinctes, chacune avec une façon prévue de l’annuler. Tout l’art consiste à choisir l’ordre des étapes, et à prévoir les annulations avant que quoi que ce soit ne tourne mal.
La façon classique de rendre tout-ou-rien des modifications dans plusieurs bases de données est la validation en deux phases, pilotée par un coordinateur. En phase un, il demande à chaque participant s’il peut valider. Chacun rend ses modifications durables, conserve ses verrous et vote oui. En votant oui, il renonce au droit d’annuler de lui-même. Si tous les votes sont oui, le coordinateur consigne sa décision : valider. En phase deux, il la communique à chaque participant, qui applique le résultat et libère ses verrous. Mais si le coordinateur plante après les votes, les participants ne peuvent pas décider seuls : ils conservent donc leurs verrous jusqu’à son rétablissement. Entre des services détenus de manière indépendante, cela convient mal. La disponibilité se multiplie à la baisse, les verrous sont maintenus à travers le réseau, les déploiements deviennent couplés, et de nombreux participants, comme un prestataire de paiement, ne peuvent pas du tout y prendre part.
L’alternative est la saga : une suite de transactions locales, chacune dans la base de données propre à un service, reliées par des messages. Chaque étape est associée à une transaction compensatoire qui l’annule au sens métier. Soit toutes les étapes aboutissent, soit celles qui ont abouti sont compensées dans l’ordre inverse. Ce qu’une saga ne vous donne pas, c’est l’isolation : entre deux étapes, d’autres peuvent voir l’état intermédiaire.
Voici la saga de commande de la livraison de repas, pilotée par le service de commandes en tant qu’orchestrateur. D’abord, il enregistre la commande comme en attente, ce qui signale aux autres que la commande est en cours de changement. Ensuite, il demande au service de paiement d’autoriser le paiement : un blocage peu coûteux sur la carte, facile à annuler. Puis il demande au service d’affectation de réserver un livreur, l’étape la plus susceptible d’échouer aux heures de pointe. Cette fois, aucun livreur n’est disponible. L’orchestrateur compense donc les étapes terminées, de la plus récente à la plus ancienne. Il annule l’autorisation de paiement. Puis il marque la commande comme rejetée, en conservant la ligne pour l’audit, et prévient le client avant que quoi que ce soit ait été cuisiné ou débité. Si un livreur avait été trouvé, l’étape quatre, la confirmation au restaurant, aurait été le pivot. Une fois qu’une cuisine commence à préparer le repas, la saga ne peut plus qu’aller de l’avant : les étapes suivantes sont donc réessayées jusqu’à ce qu’elles réussissent.
Un retour arrière fait comme si rien ne s’était passé. Une compensation est une nouvelle transaction qui contrebalance quelque chose qui a bel et bien eu lieu, et qui a pu être vu. Elle peut lever le blocage sur la carte, mais elle ne peut pas annuler l’envoi d’un e-mail. Les compensations doivent donc être idempotentes, car leur déclencheur peut arriver deux fois, et rejouables jusqu’à ce qu’elles réussissent, ou bien déclencher une alerte pour qu’une personne intervienne. Elles peuvent même arriver avant l’étape qu’elles annulent. Le service de paiement doit alors mémoriser l’annulation, et refuser l’autorisation tardive. Et les étapes qui ne peuvent pas être annulées, comme la cuisson du repas, ont leur place après le pivot, ou nécessitent une compensation métier, comme un bon d’achat ou un remboursement.
Chaque étape de saga met à jour sa base de données et envoie un message. Faire les deux séparément, c’est le problème de la double écriture : un plantage entre les deux, et le message est perdu, ou annonce quelque chose qui n’a jamais eu lieu. L’outbox transactionnelle le résout avec une seule transaction locale. Le service de commandes écrit ensemble la ligne de la commande et le message sortant, sous la forme d’une ligne dans une table outbox. Les deux sont validées, ou aucune ne l’est. Un relais séparé lit l’outbox, en l’interrogeant régulièrement ou en lisant le journal des modifications de la base de données, et publie chaque message vers le courtier. Un relais peut planter avant d’avoir noté ce qu’il a envoyé : un message peut donc partir deux fois. Le destinataire conserve donc une inbox des ID de messages traités, enregistrée avec sa modification, et ignore les doublons.
Alors, comment choisir ? Préférez une seule transaction locale quand vous le pouvez. Si un découpage en services traverse une frontière de cohérence, la solution la moins coûteuse consiste souvent à déplacer la frontière. Utilisez une saga quand les étapes peuvent être compensées ou réessayées. La chorégraphie, où les services réagissent simplement aux événements les uns des autres, convient à deux ou trois étapes. Pour des flux plus longs, avec des branchements et des délais d’expiration, la plupart des praticiens préfèrent un orchestrateur. Et ordonnez les étapes délibérément : les échecs probables avant le pivot, et tout ce qui est irréversible après.
La section complète présente le tableau détaillé des étapes, et les contre-mesures au manque d’isolation des sagas. Ensuite, dans la section six point huit, nous verrons les architectures serverless et basées sur l’espace. À très vite.
Ce qu’il faut savoir
- La validation en deux phases assure l’atomicité entre participants au moyen des phases de préparation et de validation, mais elle bloque tant que le coordinateur est en panne et maintient des verrous à travers le réseau ; XA (X/Open, 1991) la normalise.
- Entre services détenus de manière indépendante, le 2PC multiplie l’indisponibilité, maintient des verrous à distance, couple les déploiements et n’est pas pris en charge par la plupart des API ni par de nombreux courtiers : on l’évite donc en général.
- Une saga (Garcia-Molina et Salem, 1987) est une suite de transactions locales, chacune assortie d’une transaction compensatoire ; soit toutes les étapes aboutissent, soit celles qui ont abouti sont compensées dans l’ordre inverse.
- Les compensations sont de nouvelles transactions qui vont de l’avant, pas des retours arrière : des états intermédiaires ont pu être vus, et les compensations doivent être idempotentes, rejouables et capables d’arriver dans le désordre.
- Les sagas manquent d’isolation ; parmi les contre-mesures décrites par Chris Richardson figurent les verrous sémantiques, les mises à jour commutatives, la vue pessimiste, la relecture de la valeur, les fichiers de versions et le choix par la valeur.
- L’outbox transactionnelle écrit la modification métier et son message dans une même transaction locale, et un relais (par interrogation périodique ou par capture des données modifiées, comme Debezium) le publie au moins une fois ; des récepteurs idempotents dotés d’une inbox éliminent les doublons.
- Préférez une transaction locale unique quand c’est possible, revoyez les frontières quand le découpage est accidentel, et utilisez des sagas quand les étapes peuvent être compensées ou réessayées.
Le problème : une action métier, plusieurs bases de données
Dans une seule base de données, une transaction rend atomiques plusieurs modifications : la ligne de la commande, ses articles et la référence du paiement sont tous enregistrés, ou aucun ne l’est (section 0.5). Dès que les données sont réparties entre des services qui possèdent chacun leur base de données, comme le recommandent les microservices et la base de données par service, une seule action métier, comme passer une commande de repas, touche plusieurs bases de données appartenant à des services différents : celle du service de commandes, celle du service de paiement et celle du service d’affectation. Aucune transaction locale ne peut les couvrir toutes.
Il existe deux familles de réponses. La première consiste à faire agir de nombreux gestionnaires de ressources comme un seul, au moyen d’un protocole de validation atomique distribuée, dont la validation en deux phases est l’exemple classique. La seconde consiste à accepter que l’action soit une suite de transactions locales et à concevoir en fonction des moments intermédiaires : c’est la saga. Une troisième réponse, souvent la meilleure, consiste à remarquer que la frontière de la transaction et celle du service ne coïncident pas, et à déplacer l’une des deux. Cette section couvre les trois et se termine par un guide pour choisir.
La validation en deux phases et XA
La validation en deux phases (2PC) est un protocole, qui remonte à la fin des années 1970, assurant une atomicité tout-ou-rien entre plusieurs participants, par exemple deux bases de données, ou une base de données et un courtier de messages (message broker). Un coordinateur (le gestionnaire de transactions) le pilote. En phase 1, préparation, le coordinateur demande à chaque participant s’il peut valider. Un participant qui répond oui doit d’abord rendre les modifications durables et conserver les verrous concernés, car en votant oui il renonce au droit d’annuler de lui-même. Si tous les votes sont oui, le coordinateur écrit sa décision, COMMIT, dans son propre journal ; si un vote est non ou qu’un participant ne répond pas, la décision est ABORT. En phase 2, validation, il communique le résultat à chaque participant, qui l’applique et libère ses verrous.
La faiblesse est que le 2PC est un protocole bloquant. Un participant qui a voté oui est en doute : il ne connaît pas le résultat et ne peut pas le deviner sans risque, car un autre participant a peut-être déjà validé. Si le coordinateur tombe en panne à ce moment-là et le reste, les participants en doute conservent leurs verrous jusqu’à son rétablissement, et les autres transactions qui touchent ces lignes attendent elles aussi. Les opérateurs peuvent forcer une décision heuristique pour libérer les verrous, au risque de rompre l’atomicité.
La spécification XA, publiée par X/Open en 1991, a normalisé l’interface entre un gestionnaire de transactions et des gestionnaires de ressources, de sorte que, par exemple, un serveur d’applications Java puisse valider atomiquement une mise à jour de base de données et un envoi de message. De nombreuses bases de données relationnelles et certains courtiers de messages la prennent en charge.
La validation en deux phases, et là où elle bloque
Pourquoi on évite le 2PC entre microservices
Le 2PC fonctionne et reste utilisé à l’intérieur des frontières d’un même système. Entre services détenus de manière indépendante, il convient généralement mal, pour des raisons qui se renforcent mutuellement.
La disponibilité se multiplie à la baisse. Chaque participant et le coordinateur doivent être disponibles pour qu’une transaction soit validée : la disponibilité de la transaction est donc le produit des leurs, le même calcul que pour une chaîne d’appels synchrones (voir pourquoi distribuer). Les verrous sont maintenus à travers le réseau pendant au moins deux allers-retours, et bien plus longtemps si quelque chose est lent, ce qui réduit le débit et accroît la contention. Le couplage revient : tous les participants doivent parler le même protocole, partager un gestionnaire de transactions et s’accorder sur les délais d’expiration, ce qui sape la déployabilité indépendante qui justifiait de séparer les services. De nombreux participants ne peuvent pas y prendre part : la plupart des API HTTP, les services tiers comme les prestataires de paiement, de nombreuses bases de données NoSQL et plusieurs courtiers populaires ne prennent pas en charge XA. Et l’API du prestataire de paiement ne maintiendra jamais un verrou pour votre coordinateur.
Le tableau est différent à l’intérieur des bases de données distribuées. Des systèmes comme Google Spanner exécutent la validation en deux phases en interne entre des groupes de répliques eux-mêmes maintenus cohérents par consensus, si bien qu’un coordinateur tombé en panne est remplacé plutôt qu’attendu. Cela rend praticables les transactions entre partitions au sein d’une même base de données ; cela ne rend pas praticables les transactions entre des services détenus séparément, qui reposent sur des technologies différentes.
Les sagas : une suite de transactions locales
Hector Garcia-Molina et Kenneth Salem ont introduit la saga en 1987, dans un article présenté à SIGMOD sur les transactions de longue durée, celles qui maintiendraient des verrous de base de données pendant des heures. Leur idée était de découper une telle transaction en une suite de transactions ordinaires T1, T2 … Tn, dont chacune est validée séparément, et d’associer à chacune une transaction compensatoire Ci qui l’annule sur le plan sémantique. Le système garantit alors soit que la suite entière aboutit, soit que les étapes ayant abouti sont suivies de leurs compensations dans l’ordre inverse : T1 … Tj, Cj … C1.
Les architectures de microservices ont adopté cette idée parce qu’elle correspond exactement à leurs contraintes : chaque étape est une transaction ACID locale dans la base de données propre à un service, et les étapes sont reliées par des messages. Une saga offre l’atomicité au sens métier et à terme (toutes les étapes ont lieu, ou leurs effets sont annulés), mais pas l’isolation : entre deux étapes, d’autres transactions peuvent voir l’état intermédiaire.
Il est utile de classer les étapes, comme le fait Chris Richardson dans Microservices Patterns (2018). Les transactions compensables peuvent être annulées par une compensation. La transaction pivot est le point de non-retour : une fois qu’elle est validée, la saga ira jusqu’au bout. Les transactions rejouables viennent après le pivot et doivent être conçues pour finir par réussir si on les réessaie. Placer avant le pivot les étapes susceptibles d’échouer, et après lui celles qui ne peuvent pas être annulées, est une décision de conception essentielle.
Chorégraphie ou orchestration
Les étapes d’une saga doivent être coordonnées d’une manière ou d’une autre, et les deux options reprennent la chorégraphie et l’orchestration présentées à propos de l’architecture pilotée par les événements.
Dans une saga chorégraphiée, il n’y a pas de coordinateur. Chaque service écoute des événements et y réagit en effectuant sa transaction locale et en publiant un nouvel événement : le service de commandes publie OrderCreated, le service de paiement réagit et publie PaymentAuthorised, le service d’affectation réagit à son tour, et ainsi de suite ; des événements d’échec déclenchent des compensations dans les services qui les écoutent. C’est simple pour deux ou trois étapes, et cela garde les services faiblement couplés. À mesure que les étapes se multiplient, le flux n’existe plus que de façon implicite, éparpillé entre les services, si bien qu’il devient difficile à voir, à tester et à modifier ; et des dépendances cycliques entre services, par le jeu des événements, s’installent insidieusement.
Dans une saga orchestrée, un orchestrateur unique, généralement situé dans le service responsable du processus métier, envoie des commandes aux participants et réagit à leurs réponses. L’orchestrateur est une machine à états persistante : il enregistre dans sa propre base de données l’étape où il en est, ce qui lui permet de reprendre après une panne. Le flux est explicite et testable en un seul endroit, et les participants n’ont pas besoin de se connaître. Le risque est que l’orchestrateur accumule une logique métier qui relève des participants. Des moteurs de workflow comme Temporal, Camunda et AWS Step Functions fournissent une orchestration durable sous forme de plateforme, au prix d’une dépendance supplémentaire. Au-delà de trois étapes environ, ou pour tout flux comportant des branchements et des délais d’expiration, la plupart des praticiens préfèrent l’orchestration.
Exemple commenté : la saga de commande
La saga de commande de la plateforme de livraison de repas est orchestrée par le service de commandes. L’ordre des étapes est délibéré : l’autorisation de paiement est un blocage de fonds peu coûteux et facile à annuler, la réservation du livreur est l’étape la plus susceptible d’échouer aux heures de pointe, et la confirmation au restaurant est le pivot, car une fois qu’une cuisine a commencé à préparer la commande, celle-ci ne peut plus simplement être annulée.
La saga de commande, étape par étape
| Étape | Service et transaction locale | Type | Compensation si une étape ultérieure échoue |
|---|---|---|---|
| 1. Créer la commande | Le service de commandes insère la commande avec l’état PENDING (un verrou sémantique). | Compensable | C1 : passer l’état à REJECTED et enregistrer le motif. La ligne est conservée pour l’audit, et non supprimée. |
| 2. Autoriser le paiement | Le service de paiement demande au prestataire de carte de bloquer le montant total, en utilisant l’ID de la commande comme clé d’idempotence. | Compensable | C2 : annuler l’autorisation, ce qui libère le montant bloqué. Le client peut brièvement voir un montant en attente dans son application bancaire. |
| 3. Réserver un livreur | Le service d’affectation réserve un livreur proche pour le créneau de retrait. | Compensable | C3 : libérer le livreur pour qu’il puisse accepter d’autres courses. |
| 4. Confirmer la commande | Le service de commandes passe l’état à CONFIRMED et envoie le bon de commande au restaurant. | Pivot | Aucune : à partir d’ici, la saga doit aller de l’avant. Les problèmes ultérieurs sont traités par de nouveaux processus métier (remboursements, bons d’achat). |
| 5. Avertir le client ; encaisser le paiement au retrait | Le service de notification envoie « Commande confirmée » ; le service de paiement encaisse le montant bloqué quand le livreur récupère le repas. | Rejouable | Aucune : ces étapes sont réessayées, de façon idempotente, jusqu’à ce qu’elles réussissent ou soient transmises à une personne. |
La saga de commande quand aucun livreur n’est disponible
La compensation n’est pas un retour arrière
Un retour arrière (rollback) de base de données fait comme si la transaction n’avait jamais eu lieu : personne d’autre n’a jamais vu ses modifications. Une compensation est une nouvelle transaction, tournée vers l’avant, qui contrebalance sur le plan sémantique une transaction antérieure qui a bel et bien eu lieu et qui a pu être vue. Dans la saga de commande, la banque du client affiche peut-être déjà un montant bloqué, et un e-mail a peut-être annoncé « paiement autorisé ». La compensation ne peut pas annuler l’envoi de l’e-mail ; elle peut seulement lever le blocage et envoyer un autre message. Certaines actions ne peuvent pas du tout être compensées, comme un repas déjà cuisiné ou une notification déjà lue ; elles ont leur place après le pivot, ou nécessitent une compensation métier comme un bon d’achat ou un remboursement.
Les compensations demandent donc le même soin que les étapes qui vont de l’avant. Elles doivent être idempotentes, car le message qui les déclenche peut être livré deux fois. Elles doivent être rejouables jusqu’à ce qu’elles réussissent, car une compensation qui échoue définitivement laisse le système dans un état que personne n’a conçu ; si elles ne peuvent vraiment pas réussir, elles doivent déclencher une alerte pour qu’une personne règle le problème. Elles peuvent devoir être commutatives avec l’étape qu’elles compensent : si un VoidAuthorisation arrive avant l’AuthorisePayment qu’il annule (ce qui est possible après des délais d’expiration et des nouvelles tentatives), le service de paiement doit mémoriser l’annulation et refuser l’autorisation tardive plutôt qu’échouer. Enfin, elles doivent être visibles pour le métier : « rejetée, pas de livreur » est une information que l’équipe des opérations veut voir sur un tableau de bord.
Vivre sans isolation : anomalies et contre-mesures
Comme chaque étape est validée séparément, les sagas n’ont pas le I d’ACID, et des sagas concurrentes peuvent interférer. Richardson décrit trois anomalies qui en résultent. Une mise à jour perdue se produit quand une saga écrase une modification faite par une autre sans l’avoir lue, par exemple quand une saga d’annulation marque une commande comme annulée et que la saga de commande la passe ensuite à confirmée. Une lecture sale se produit quand une saga lit des données qu’une autre saga a modifiées mais risque encore de compenser, comme une vérification de plafond de crédit qui compte un blocage sur le point d’être annulé. Une lecture floue, ou non reproductible, se produit quand deux étapes d’une même saga lisent les mêmes données et obtiennent des valeurs différentes parce qu’une autre saga les a modifiées entre-temps.
Il recense aussi des contre-mesures, des techniques de conception qui réduisent ces anomalies, en s’appuyant sur des travaux de recherche antérieurs consacrés aux transactions portant sur plusieurs bases de données. Voici les plus utilisées.
Contre-mesures au manque d’isolation des sagas (d’après Richardson)
| Contre-mesure | Idée | Exemple de la livraison de repas |
|---|---|---|
| Verrou sémantique | Une étape compensable positionne un indicateur au niveau de l’application, généralement un état *_PENDING, qui signale aux autres transactions que l’enregistrement est en cours de changement ; elles attendent, échouent ou le traitent de façon particulière. | Tant qu’une commande est PENDING, une demande d’annulation est mise en file d’attente ou refusée avec le message « commande en cours de traitement », au lieu d’être appliquée à mi-parcours. |
| Mises à jour commutatives | Concevoir les mises à jour pour qu’elles donnent le même résultat dans n’importe quel ordre, ce qui supprime les mises à jour perdues. | Le registre des gains du livreur enregistre les crédits et les débits comme des écritures distinctes au lieu d’écraser un solde. |
| Vue pessimiste | Réordonner les étapes pour réduire au minimum le risque métier d’une lecture sale, en plaçant les mises à jour risquées dans des étapes rejouables après le pivot. | N’augmenter la « capacité disponible » d’un restaurant que dans une étape rejouable, une fois l’annulation certaine, jamais dans une étape susceptible d’être compensée. |
| Relecture de la valeur | Avant de mettre à jour, relire l’enregistrement et vérifier qu’il n’a pas changé depuis sa lecture (verrouillage optimiste) ; s’il a changé, abandonner et éventuellement relancer la saga. | Avant de confirmer, l’orchestrateur vérifie que la version de la commande est toujours celle qu’il a créée ; si une annulation l’a modifiée, il compense à la place. |
| Fichier de versions | Enregistrer les opérations appliquées à un enregistrement, afin que les opérations arrivées dans le désordre puissent être réordonnées ou reconnues. | Le service de paiement enregistre qu’une annulation est arrivée pour une autorisation qu’il n’a pas encore vue, et rejette l’autorisation tardive. |
| Par la valeur | Choisir le mécanisme de coordination de chaque demande selon son risque métier. | Les commandes ordinaires passent par la saga ; une grosse commande de traiteur pour une entreprise pourrait suivre un processus plus strict et plus lent. |
Publier de façon fiable : l’outbox transactionnelle
Chaque étape de saga fait deux choses : elle met à jour sa propre base de données et elle envoie un message (un événement ou une réponse). Les faire séparément crée le problème de la double écriture (dual write). Si le service valide puis plante avant de publier, le reste de la saga n’entend jamais parler de l’étape. S’il publie d’abord et que la validation échoue ensuite, d’autres services agissent sur quelque chose qui n’a jamais eu lieu. Envelopper la base de données et le courtier dans un 2PC est précisément ce que nous voulions éviter.
L’outbox transactionnelle (transactional outbox) le résout avec une seule transaction locale. Le service écrit la modification métier et le message sortant, sous la forme d’une ligne dans une table outbox, dans la même transaction de base de données ; soit les deux sont validés, soit aucun ne l’est. Un relais séparé lit ensuite l’outbox et publie chaque message vers le courtier. Deux conceptions de relais sont courantes. Un éditeur par interrogation (polling publisher) interroge régulièrement la table outbox pour y trouver les lignes non publiées et les marque comme envoyées : c’est simple, mais cela ajoute de la charge et de la latence. La lecture du journal des transactions (transaction log tailing) utilise la capture des données modifiées (CDC) : un outil lit le journal de réplication de la base de données elle-même (le write-ahead log de PostgreSQL ou le binary log de MySQL) et transforme les nouvelles lignes de l’outbox en messages. Debezium, une plateforme de CDC open source généralement exécutée avec Kafka Connect, en est l’exemple le plus connu ; elle comprend un routeur d’événements outbox (outbox event router) qui achemine chaque ligne vers un topic en fonction d’une colonne comme le type d’agrégat.
Le relais peut planter après avoir publié et avant d’avoir enregistré qu’il l’a fait : il publiera donc parfois un message deux fois. L’outbox assure ainsi une livraison au moins une fois, et les consommateurs doivent gérer les doublons. L’ordre de publication est préservé pour chaque agrégat si le relais publie les lignes dans l’ordre de validation et utilise l’ID de l’agrégat comme clé de message.
BEGIN;
INSERT INTO orders (id, customer_id, state, total_pence)
VALUES ('1042', 'c-77', 'PENDING', 2350);
INSERT INTO outbox (id, aggregate_type, aggregate_id, type, payload)
VALUES ('e-9f1c', 'order', '1042', 'OrderCreated',
'{"orderId":"1042","customerId":"c-77","totalPence":2350}');
COMMIT;
-- A relay (poller or CDC such as Debezium) later publishes e-9f1c to the broker.L’outbox à la sortie, l’inbox à l’entrée
Recevoir de façon fiable : récepteurs idempotents et inbox
Comme l’outbox, les nouvelles tentatives du courtier et celles de la saga produisent toutes des doublons, chaque participant doit être un récepteur idempotent (Idempotent Receiver), nom que Gregor Hohpe et Bobby Woolf ont donné à ce patron dans Enterprise Integration Patterns (2003). Certaines opérations sont naturellement idempotentes (« passer la commande 1042 à CONFIRMED ») ; d’autres nécessitent un contrôle des doublons. L’implémentation habituelle est une inbox : une table des ID de messages traités, écrite dans la même transaction locale que la modification métier, de sorte que « traiter le message » et « se souvenir qu’il a été traité » soient validés ensemble. Un message livré une nouvelle fois trouve son ID déjà présent et est acquitté sans rien faire. Le code côté consommateur figure dans la section sur l’architecture pilotée par les événements.
Ensemble, l’outbox, la livraison au moins une fois et les récepteurs idempotents donnent le résultat pratique que l’on désigne généralement par « exactement une fois » : chaque effet métier se produit une seule fois, même si les messages peuvent circuler plus d’une fois.
Guide de décision
Guide de décision : une transaction, une saga ou de nouvelles frontières ?
Utilisez-le quand une action métier doit modifier des données à plus d’un endroit.
- Toutes les données qui doivent changer ensemble appartiennent-elles à un seul service et sont-elles stockées dans une seule base de données ?OuiUtilisez une seule transaction ACID locale. C’est l’option la plus simple et la plus solide ; ne la distribuez pas.NonPassez à la question suivante.
- Les parties qui doivent changer ensemble changent-elles presque toujours ensemble, appartiennent-elles à une seule équipe et partagent-elles un contexte délimité, si bien que le découpage est accidentel ?OuiRevoyez la frontière : fusionnez les services, ou déplacez les données, pour que l’action devienne une seule transaction locale. Les sagas ne corrigent pas un mauvais découpage.NonPassez à la question suivante.
- Chaque participant doit-il voir toutes les modifications au même instant, sans qu’aucun état intermédiaire ne soit jamais visible (par exemple, un registre comptable qui doit toujours être équilibré) ?OuiGardez ces enregistrements dans une seule base de données, ou utilisez une base de données distribuée qui fournit en interne des transactions entre partitions. N’utilisez le 2PC qu’à l’intérieur des frontières d’un même système, avec des participants que vous maîtrisez.NonPassez à la question suivante.
- Chaque étape peut-elle soit être compensée, soit être placée après un pivot et réessayée jusqu’à ce qu’elle réussisse ?OuiUtilisez une saga : orchestrée si elle compte plus de trois étapes environ ou le moindre branchement, chorégraphiée si elle est courte et stable. Ajoutez une outbox, des récepteurs idempotents et des contre-mesures comme les verrous sémantiques.NonRéordonnez les étapes ou modifiez le processus métier (par exemple, réserver avant de débiter, ou ajouter une vérification manuelle) jusqu’à ce que ce soit le cas ; sinon, reconsidérez les frontières.
Vérifiez votre compréhension
Pourquoi un participant à une validation en deux phases ne peut-il pas simplement valider de lui-même si le coordinateur disparaît après qu’il a voté oui ?
Parce qu’il ne connaît pas la décision : un autre participant a peut-être voté non, et le coordinateur a donc peut-être décidé d’annuler ; deviner pourrait rompre l’atomicité.
Dans la saga de commande, pourquoi la confirmation de la commande au restaurant est-elle le pivot plutôt que la première étape ?
Une fois que la cuisine commence à préparer le repas, l’étape ne peut plus être annulée : elle doit donc venir après les étapes susceptibles d’échouer (autorisation de paiement, réservation du livreur), qui peuvent être compensées à peu de frais.
Quel problème l’outbox transactionnelle résout-elle, et quelle garantie offre-t-elle ?
Elle supprime la double écriture entre la base de données et le courtier en validant ensemble la modification et le message ; le relais livre ensuite le message au moins une fois, si bien que des doublons sont possibles.
Citez deux différences entre une compensation et un retour arrière de base de données.
C’est une nouvelle transaction qui s’exécute après la validation de la transaction d’origine, et d’autres ont peut-être déjà vu celle-ci ou agi en conséquence ; elle peut aussi n’être qu’une annulation sémantique (un remboursement, une annulation d’autorisation) plutôt qu’un retour exact à l’état précédent.
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 validation de commande appelle le paiement, puis le stock, puis l’expédition, et il nous arrive de débiter des clients pour des articles que nous ne pouvons pas expédier. Comment la repenser sous forme de saga ? »