Maison > développement back-end > Golang > Transactions dans les microservices : partie modèle SAGA avec chorégraphie

Transactions dans les microservices : partie modèle SAGA avec chorégraphie

Barbara Streisand
Libérer: 2025-01-23 02:05:08
original
459 Les gens l'ont consulté

Dans le premier article de cette série, nous avons présenté le modèle SAGA et démontré comment une Orchestration minimale peut gérer des transactions distribuées avec un orchestrateur central.

Soyons réalistes ! Cette fois, nous allons plonger dans l'Approche chorégraphique, où les services coordonnent les flux de travail en émettant et en consommant des événements de manière autonome.

Pour rendre cela pratique, nous allons mettre en œuvre un flux de travail de soins de santé multiservice à l'aide de Go et RabbitMQ. Chaque service aura son propre main.go, ce qui facilitera sa mise à l'échelle, son test et son exécution indépendante.

Qu’est-ce que la chorégraphie SAGA ?

La chorégraphie repose sur une communication décentralisée. Chaque service écoute les événements et déclenche les étapes suivantes en émettant de nouveaux événements. Il n’y a pas d’orchestrateur central ; le flux émerge des interactions des services individuels.

Avantages clés :

  • Services découplés : Chaque service fonctionne de manière indépendante.
  • Évolutivité : Les systèmes pilotés par événements gèrent efficacement des charges élevées.
  • Flexibilité : L'ajout de nouveaux services ne nécessite pas de modifier la logique du flux de travail.

Défis :

  • Complexité du débogage : Le suivi des événements sur plusieurs services peut être délicat. (Je vais écrire un article dédié à ce sujet, restez connectés !)
  • Configuration de l'infrastructure : Les services nécessitent un courtier de messages robuste (par exemple, RabbitMQ) pour relier tous les points.
  • Tempêtes d'événements : Des flux de travail mal conçus peuvent submerger le système d'événements.

Exemple pratique : flux de travail de soins de santé

Revoyons notre workflow de soins de santé dès le premier article :

  1. Service aux patients : Vérifie les détails du patient et la couverture d'assurance.
  2. Service de planification : Planifie la procédure.
  3. Service d'inventaire :Réserve les fournitures médicales.
  4. Service de facturation : Traite la facturation.

Chaque service :

  • Écoutez des événements spécifiques à l'aide de RabbitMQ.
  • Émettez de nouveaux événements pour déclencher les étapes suivantes.

Configuration de RabbitMQ avec Docker

Nous utiliserons RabbitMQ comme file d'attente des événements. Exécutez-le localement à l'aide de Docker :

docker run --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.0.5-management
Copier après la connexion
Copier après la connexion

Accédez à l'interface de gestion RabbitMQ sur http://localhost:15672 (nom d'utilisateur : invité, mot de passe : invité).

Configuration des échanges, des files d'attente et des liaisons

Nous devons configurer RabbitMQ pour accueillir nos événements. Voici un exemple de fichier init.go pour configurer l'infrastructure RabbitMQ :

package main

import (
    "log"

    "github.com/rabbitmq/amqp091-go"
)

func main() {
    conn, err := amqp091.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %v", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %v", err)
    }
    defer ch.Close()

    err = ch.ExchangeDeclare("events", "direct", true, false, false, false, nil)
    if err != nil {
        log.Fatalf("Failed to declare an exchange: %v", err)
    }

    _, err = ch.QueueDeclare("PatientVerified", true, false, false, false, nil)
    if err != nil {
        log.Fatalf("Failed to declare a queue: %v", err)
    }

    err = ch.QueueBind("PatientVerified", "PatientVerified", "events", false, nil)
    if err != nil {
        log.Fatalf("Failed to bind a queue: %v", err)
    }
}
Copier après la connexion
Copier après la connexion

Code complet ici !

Remarque : Dans un environnement de production, vous souhaiterez peut-être gérer cette configuration à l'aide d'une approche GitOps (par exemple, avec Terraform) ou laisser chaque service gérer ses propres files d'attente de manière dynamique.

Implémentation : fichiers de service

Chaque service aura son propre main.go. Nous inclurons également des actions de compensation pour gérer les échecs avec élégance.

1. Service aux patients

Ce service vérifie les détails du patient et émet un événement PatientVerified. Il compense également en avertissant le patient si une panne en aval survient.

docker run --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.0.5-management
Copier après la connexion
Copier après la connexion

2. Service de planification

Ce service écoute PatientVerified et émet ProcedureScheduled. Il compense en annulant la procédure si une panne en aval survient.

package main

import (
    "log"

    "github.com/rabbitmq/amqp091-go"
)

func main() {
    conn, err := amqp091.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %v", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %v", err)
    }
    defer ch.Close()

    err = ch.ExchangeDeclare("events", "direct", true, false, false, false, nil)
    if err != nil {
        log.Fatalf("Failed to declare an exchange: %v", err)
    }

    _, err = ch.QueueDeclare("PatientVerified", true, false, false, false, nil)
    if err != nil {
        log.Fatalf("Failed to declare a queue: %v", err)
    }

    err = ch.QueueBind("PatientVerified", "PatientVerified", "events", false, nil)
    if err != nil {
        log.Fatalf("Failed to bind a queue: %v", err)
    }
}
Copier après la connexion
Copier après la connexion

Services supplémentaires

Inclure les implémentations du service d'inventaire et du service de facturation, en suivant la même structure que ci-dessus. Chaque service écoute l'événement précédent et émet le suivant, garantissant ainsi la logique de compensation en cas d'échec.

Code complet ici !


Exécution du flux de travail

Démarrez RabbitMQ :

// patient/main.go
package main

import (
    "fmt"
    "log"

    "github.com/rabbitmq/amqp091-go"
    "github.com/thegoodapi/saga_tutorial/choreography/common"
)

func main() {
    conn, err := amqp091.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %v", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %v", err)
    }
    defer ch.Close()

    go func() {
        fmt.Println("[PatientService] Waiting for events...")
        msgs, err := common.ConsumeEvent(ch, "ProcedureScheduleCancelled")
        if err != nil {
            log.Fatalf("Failed to consume event: %v", err)
        }

        for range msgs {
            fmt.Println("[PatientService] Processing event: ProcedureScheduleCancelled")
            if err := notifyProcedureScheduleCancellation(); err != nil {
                log.Fatalf("Failed to notify patient: %v", err)
            }
        }
    }()

    common.PublishEvent(ch, "events", "PatientVerified", "Patient details verified")
    fmt.Println("[PatientService] Event published: PatientVerified")

    select {}
}

func notifyProcedureScheduleCancellation() error {
    fmt.Println("Compensation: Notify patient of procedure cancellation.")
    return nil
}
Copier après la connexion

Exécuter chaque service :
Ouvrez des terminaux séparés et exécutez :

// scheduler/main.go
package main

import (
    "fmt"
    "log"

    "github.com/rabbitmq/amqp091-go"
    "github.com/thegoodapi/saga_tutorial/choreography/common"
)

func main() {
    conn, err := amqp091.Dial("amqp://guest:guest@localhost:5672/")
    if err != nil {
        log.Fatalf("Failed to connect to RabbitMQ: %v", err)
    }
    defer conn.Close()

    ch, err := conn.Channel()
    if err != nil {
        log.Fatalf("Failed to open a channel: %v", err)
    }
    defer ch.Close()

    go func() {
        fmt.Println("[SchedulerService] Waiting for events...")
        msgs, err := common.ConsumeEvent(ch, "PatientVerified")
        if err != nil {
            log.Fatalf("Failed to consume event: %v", err)
        }

        for range msgs {
            fmt.Println("[SchedulerService] Processing event: PatientVerified")
            if err := scheduleProcedure(); err != nil {
                common.PublishEvent(ch, "events", "ProcedureScheduleFailed", "Failed to schedule procedure")
                fmt.Println("[SchedulerService] Compensation triggered: ProcedureScheduleFailed")
            } else {
                common.PublishEvent(ch, "events", "ProcedureScheduled", "Procedure scheduled successfully")
                fmt.Println("[SchedulerService] Event published: ProcedureScheduled")
            }
        }
    }()

    select {}
}

func scheduleProcedure() error {
    fmt.Println("Step 2: Scheduling procedure...")
    return nil // or simulate a failure
}
Copier après la connexion

Observer la sortie :
Chaque service traite les événements dans l'ordre, enregistrant la progression du flux de travail.

Ce qui s'est passé?

Décomposons-le !

Tout d'abord, aux fins de cet article, nous n'implémentons pas SuppliesReserveFailed et ProcedureScheduleFailed, pour éviter une complexité inutile.

Nous mettons en œuvre les événements suivants

Étapes (ou transactions) :

  • T1 : (init) : PatientVérifié
  • T2 : ProcédurePlanifiée
  • T3 : FournituresRéservées
  • T4 : Facturation réussie

Rémunérations :

  • C4 : Échec de facturation
  • C3 : ReservedSuppliesReleased
  • C2 : ProcédureScheduleAnnulée
  • C1 : NotifyFailureToUser (non implémenté)

En suivant ce schéma de mise en œuvre

high-level implementation flow

Ce diagramme représente une approche courante pour documenter la chorégraphie. Cependant, je trouve cela quelque peu difficile à comprendre et un peu frustrant, en particulier pour ceux qui ne connaissent pas l'implémentation ou le modèle.

Décomposons-le !

detailed implementation flow

Le diagramme ci-dessus est beaucoup plus détaillé et décompose chaque étape, ce qui permet de mieux comprendre ce qui se passe.

En un mot :

  1. Le service aux patients vérifie les détails du patient avec succès
  2. Le service aux patients émet PatientVerified
  3. Service de planification consomme PatientVérifié
  4. Le service de planification planifie le rendez-vous avec succès
  5. Le service de planification émet ProcedureScheduled
  6. Service d'inventaire consomme ProcédurePlanifiée
  7. Le service d'inventaire réserve les fournitures avec succès
  8. Service d'inventaire émet FournituresRéservé
  9. Service de facturation consomme FournituresRéservées
  10. Le service de facturation ne parvient pas à facturer le client et démarre la compensation
  11. Le service de facturation émet BillingFailed
  12. Le service d'inventaire consomme BillingFailed
  13. Le service inventaire libère les fournitures, réservées à l'étape 7
  14. Le service d'inventaire émet ReservedSuppliesReleased
  15. Le service de planification consomme ReservedSuppliesReleased
  16. Le service Planificateur supprime le rendez-vous programmé à l'étape 4
  17. Le service de planification émet ProcedureScheduleCancelled
  18. Le service aux patients consomme ProcedureScheduleCancelled
  19. Le service patient informe le client de l'erreur

Notez que nous n'implémentons pas les échecs pour les étapes 1, 4 et 7 par souci de concision ; cependant, l'approche serait la même. Chacun de ces échecs déclencherait un retour en arrière des étapes précédentes.


Observabilité

L'observabilité est essentielle pour le débogage et la surveillance des systèmes distribués. La mise en œuvre de journaux, métriques et traces garantit que les développeurs peuvent comprendre le comportement du système et diagnostiquer efficacement les problèmes.

Enregistrement

  • Utilisez la journalisation structurée (par exemple, format JSON) pour capturer les événements et les métadonnées.
  • Incluez les ID de corrélation dans les journaux pour suivre les flux de travail entre les services.

Métrique

  • Surveillez la taille des files d'attente et les temps de traitement des événements.
  • Utilisez des outils comme Prometheus pour collecter et visualiser des métriques.

Tracé

  • Implémentez le traçage distribué (par exemple, avec OpenTelemetry) pour suivre les événements entre les services.
  • Annotez les périodes avec des données pertinentes (par exemple, noms d'événements, horodatages) pour de meilleures informations.

Nous aborderons l'observabilité en choérographie plus tard dans cette série, restez à l'écoute !


Points clés à retenir

  • Contrôle décentralisé : La chorégraphie permet une collaboration autonome.
  • Simplicité basée sur les événements : RabbitMQ simplifie l'échange de messages.
  • Architecture évolutive : L'ajout de nouveaux services se fait en toute transparence.
  • La Choérographie peut être très accablante au début, mais comme toujours : la pratique vous rend parfait meilleur !

Restez à l’écoute pour le prochain article, où nous explorerons l’Orchestration !

Consultez le référentiel complet de cette série ici. Discutons-en dans les commentaires !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal