Maison  >  Article  >  Java  >  Comment le modèle Saga résout les problèmes de transactions distribuées : méthodes et exemple concret

Comment le modèle Saga résout les problèmes de transactions distribuées : méthodes et exemple concret

Linda Hamilton
Linda Hamiltonoriginal
2024-10-20 20:11:02487parcourir

1. Comprendre le problème : la complexité des transactions distribuées

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

Les transactions distribuées impliquent plusieurs microservices, où chaque service effectue une partie d'une transaction. Par exemple, une plateforme de commerce électronique peut impliquer des services tels que le paiement, l’inventaire et la gestion des commandes. Ces services doivent travailler ensemble pour finaliser une transaction. Cependant, que se passe-t-il si l’un de ces services tombe en panne ?

1.1 Un scénario du monde réel

Imaginez une application e-commerce où les étapes suivantes se déroulent lors d'une passation de commande :

  • Étape 1 : Déduire le paiement du compte client.
  • Étape 2 : Réduisez le nombre d'articles dans l'inventaire.
  • Étape 3 : Créez une commande dans le système de gestion des commandes.

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

Si le service d'inventaire échoue après la déduction du paiement mais avant la création de la commande, le système se retrouve dans un état incohérent. Le client est facturé, mais aucune commande n'est passée.

1.2 Solutions traditionnelles et leurs limites

Pour gérer de tels échecs, on pourrait envisager d'utiliser une transaction distribuée avec un protocole de validation en deux phases. Cependant, cela introduit plusieurs problèmes :

  • Latence élevée : Chaque service doit verrouiller les ressources pendant la transaction, ce qui entraîne une latence accrue.
  • Disponibilité réduite : Si un service échoue, l'intégralité de la transaction est annulée, réduisant ainsi la disponibilité globale du système.
  • Couplage étroit : les services deviennent étroitement couplés, ce qui rend plus difficile la mise à l'échelle ou la modification de services individuels.

2. Comment le modèle Saga résout le problème

Dans les systèmes distribués, les transactions s'étendent souvent sur plusieurs microservices. Veiller à ce que tous les services soient exécutés avec succès ou pas du tout est un véritable défi. La manière traditionnelle de gérer cela (en utilisant des transactions distribuées avec une validation en deux phases) peut s'avérer problématique en raison de problèmes tels qu'une latence élevée, un couplage étroit et une disponibilité réduite.

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

Le patron Saga offre une approche plus flexible. Au lieu de tenter d'exécuter une transaction comme une seule unité, le modèle Saga décompose la transaction en étapes plus petites et isolées qui peuvent être exécutées indépendamment. Chaque étape est une transaction locale qui met à jour la base de données puis déclenche l'étape suivante. Si une étape échoue, le système effectue des actions compensatoires pour annuler les modifications apportées par les étapes précédentes, garantissant ainsi que le système peut revenir à un état cohérent.

2.1 Qu'est-ce que le modèle de saga ?

Le modèle Saga est essentiellement une séquence de transactions plus petites qui sont exécutées les unes après les autres. Voici comment cela fonctionne :

  • Transactions locales : Chaque service impliqué dans la transaction effectue sa propre transaction locale. Par exemple, dans un système de traitement des commandes, un service peut gérer le paiement, un autre l'inventaire et encore un autre l'enregistrement des commandes.
  • Publication d'un événement ou d'un message : Une fois qu'un service a terminé sa transaction locale, il publie un événement ou envoie un message indiquant la réussite de cette étape. Par exemple, une fois le paiement traité, le service de paiement peut publier un événement « PaymentCompleted ».
  • Déclenchement de l'étape suivante : Le service suivant dans la séquence écoute l'événement et, dès sa réception, procède à sa transaction locale. Cela continue jusqu'à ce que toutes les étapes de la transaction soient terminées.
  • Actions compensatoires : Si une étape échoue, des actions compensatoires sont invoquées. Ces actions sont conçues pour annuler les modifications apportées par les étapes précédentes. Par exemple, si la réduction des stocks échoue après le paiement, une action compensatoire rembourserait le paiement.

2.2 Types de sagas

Il existe deux manières principales de mettre en œuvre le modèle Saga : Chorégraphie et Orchestration.

2.2.1 Saga chorégraphique

Dans une Saga Chorégraphique, il n’y a pas de coordinateur central. Au lieu de cela, chaque service impliqué dans la Saga écoute les événements et décide quand agir en fonction du résultat des étapes précédentes. Cette approche est décentralisée et permet aux services de fonctionner de manière indépendante. Voici comment cela fonctionne :

  • Coordination basée sur les événements : Chaque service est chargé de gérer les événements qui le concernent. Par exemple, une fois que le service de paiement a traité un paiement, il émet un événement « PaymentCompleted ». Le service d'inventaire écoute cet événement et, lorsqu'il le reçoit, déduit le nombre d'articles.
  • Contrôle décentralisé : Puisqu'il n'y a pas de coordinateur central, chaque service doit savoir quoi faire ensuite en fonction des événements qu'il reçoit. Cela donne au système plus de flexibilité mais nécessite une planification minutieuse pour garantir que tous les services comprennent la séquence correcte des opérations.
  • Actions compensatoires : Si un service détecte que quelque chose s'est mal passé, il peut émettre un événement d'échec, que d'autres services écoutent pour déclencher des actions compensatoires. Par exemple, si le service d'inventaire ne peut pas mettre à jour l'inventaire, il peut émettre un événement « InventoryUpdateFailed », que le service de paiement écoute pour déclencher un remboursement.

Avantages de la chorégraphie :

  • Couplage lâche : les services sont faiblement couplés, ce qui facilite la mise à l'échelle et la modification des services individuels.
  • Résilience : Puisque chaque service agit indépendamment, le système peut être plus résilient aux pannes des services individuels.

Défis de la chorégraphie :

  • Complexité : À mesure que le nombre de services augmente, gérer et comprendre le flux des événements peut devenir complexe.
  • Manque de contrôle central : Sans coordinateur central, il peut être plus difficile de surveiller et de déboguer le flux global des transactions.

2.2.2 La saga des orchestrations

Dans une Orchestration Saga, un orchestrateur central contrôle le flux de la transaction. L'orchestrateur détermine la séquence des étapes et gère la communication entre les services. Voici comment cela fonctionne :

  • Contrôle centralisé : L'orchestrateur envoie des commandes à chaque service en séquence. Par exemple, l'orchestrateur peut d'abord demander au service de paiement de traiter un paiement. Une fois cela fait, il demande au service d'inventaire de mettre à jour l'inventaire, et ainsi de suite.
  • Exécution séquentielle : Chaque service exécute sa tâche uniquement sur instruction de l'orchestrateur, garantissant que les étapes se déroulent dans le bon ordre.
  • Logique de compensation : L'orchestrateur est également chargé d'initier des actions de compensation en cas de problème. Par exemple, si la mise à jour de l'inventaire échoue, l'orchestrateur peut ordonner au service de paiement de rembourser le paiement.

Avantages de l'orchestration :

  • Contrôle centralisé : Avec un seul orchestrateur, il est plus facile de surveiller, gérer et déboguer le flux de transactions.
  • Logique plus simple : Puisque l'orchestrateur gère le flux, les services individuels n'ont pas besoin d'être conscients de la séquence globale des transactions.

Défis de l'orchestration :

  • Point de défaillance unique : L'orchestrateur peut devenir un goulot d'étranglement ou un point de défaillance unique s'il n'est pas conçu pour une haute disponibilité.
  • Couplage étroit à l'orchestrateur : Les services dépendent de l'orchestrateur, ce qui peut rendre le système moins flexible par rapport à la chorégraphie.

3. Implémentation du modèle Simple Orchestration Saga : un guide étape par étape

Considérons le scénario du commerce électronique et implémentons-le en utilisant le modèle Saga.

Dans notre scénario d'achat de café, chaque service représente une transaction locale. Le Service Café agit comme l'orchestrateur de cette saga, coordonnant les autres services pour finaliser l'achat.

Voici un aperçu du fonctionnement de la saga :

  • Le client passe une commande : Le client passe une commande via le service de commande.
  • Coffee Service initie la saga : Le Coffee Service reçoit la commande et initie la saga.
  • Le service de commande crée une commande : Le service de commande crée une nouvelle commande et la conserve.
  • Le service de facturation calcule le coût : Le service de facturation calcule le coût total de la commande et crée un enregistrement de facturation.
  • Le Service de Paiement traite le paiement : Le Service de Paiement traite le paiement.
  • Coffee Service met à jour le statut de la commande : Une fois le paiement réussi, le Coffee Service met à jour le statut de la commande à "terminé".

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

3.1 Entité transactionnelle

How the Saga Pattern Resolves Distributed Transaction Issues: Methods and Real-World Example

Dans mon implémentation de la saga, chaque SagaItemBuilder représente une étape dans notre flux de transactions distribuées. Le ActionBuilder définit les actions à effectuer, y compris l'action principale et l'action de restauration qui sera exécutée si une erreur survient. Le ActionBuilder encapsule trois éléments d'information :

component : L'instance du bean où réside la méthode à invoquer.

method : Le nom de la méthode à appeler.

args : Les arguments à passer à la méthode.

ActionBuilder

public class ActionBuilder {
    private Object component;
    private String method;
    private Object[] args;

    public static ActionBuilder builder() {
        return new ActionBuilder();
    }

    public ActionBuilder component(Object component) {
        this.component = component;
        return this;
    }

    public ActionBuilder method(String method) {
        this.method = method;
        return this;
    }

    public ActionBuilder args(Object... args) {
        this.args = args;
        return this;
    }

    public Object getComponent() { return component; }
    public String getMethod() { return method; }
    public Object[] getArgs() { return args; }
}

SagaItemBuilder

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class SagaItemBuilder {
    private ActionBuilder action;
    private Map<Class<? extends Exception>, ActionBuilder> onBehaviour;

    public static SagaItemBuilder builder() {
        return new SagaItemBuilder();
    }

    public SagaItemBuilder action(ActionBuilder action) {
        this.action = action;
        return this;
    }

    public SagaItemBuilder onBehaviour(Class<? extends Exception> exception, ActionBuilder action) {
        if (Objects.isNull(onBehaviour)) onBehaviour = new HashMap<>();
        onBehaviour.put(exception, action);
        return this;
    }

    public ActionBuilder getAction() {
        return action;
    }

    public Map<Class<? extends Exception>, ActionBuilder> getBehaviour() {
        return onBehaviour;
    }
}

Scénarios

import java.util.ArrayList;
import java.util.List;

public class Scenarios {
    List<SagaItemBuilder> scenarios;

    public static Scenarios builder() {
        return new Scenarios();
    }

    public Scenarios scenario(SagaItemBuilder sagaItemBuilder) {
        if (scenarios == null) scenarios = new ArrayList<>();
        scenarios.add(sagaItemBuilder);
        return this;
    }

    public List<SagaItemBuilder> getScenario() {
        return scenarios;
    }
}

Voici comment puis-je valider la transaction de distribution.

package com.example.demo.saga;

import com.example.demo.saga.exception.CanNotRollbackException;
import com.example.demo.saga.exception.RollBackException;
import com.example.demo.saga.pojo.ActionBuilder;
import com.example.demo.saga.pojo.SagaItemBuilder;
import com.example.demo.saga.pojo.Scenarios;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;

@Component
public class DTC {

    public boolean commit(Scenarios scenarios) throws Exception {
        validate(scenarios);
        for (int i = 0; i < scenarios.getScenario().size(); i++) {
            SagaItemBuilder scenario = scenarios.getScenario().get(i);
            ActionBuilder action = scenario.getAction();
            Object bean = action.getComponent();
            String method = action.getMethod();
            Object[] args = action.getArgs();

            try {
                invoke(bean, method, args);
            } catch (Exception e) {
                rollback(scenarios, i, e);
                return false;
            }
        }
        return true;
    }

    private void rollback(Scenarios scenarios, Integer failStep, Exception currentStepFailException) {
        for (int i = failStep; i >= 0; i--) {
            SagaItemBuilder scenario = scenarios.getScenario().get(i);
            Map<Class<? extends Exception>, ActionBuilder> behaviours = scenario.getBehaviour();
            Set<Class<? extends Exception>> exceptions = behaviours.keySet();
            ActionBuilder actionWhenException = null;

            if (failStep == i) {
                for(Class<? extends Exception> exception: exceptions) {
                    if (exception.isInstance(currentStepFailException)) {
                        actionWhenException = behaviours.get(exception);
                    }
                }
                if (actionWhenException == null) actionWhenException = behaviours.get(RollBackException.class);
            } else {
                actionWhenException = behaviours.get(RollBackException.class);
            }

            Object bean = actionWhenException.getComponent();
            String method = actionWhenException.getMethod();
            Object[] args = actionWhenException.getArgs();
            try {
                invoke(bean, method, args);
            } catch (Exception e) {
                throw new CanNotRollbackException("Error in %s belong to %s. Can not rollback transaction".formatted(method, bean.getClass()));
            }
        }
    }

    private void validate(Scenarios scenarios) throws Exception {
        for (int i = 0; i < scenarios.getScenario().size(); i++) {
            SagaItemBuilder scenario = scenarios.getScenario().get(i);
            ActionBuilder action = scenario.getAction();
            if (action.getComponent() == null) throw new Exception("Missing bean in scenario");
            if (action.getMethod() == null) throw new Exception("Missing method in scenario");

            Map<Class<? extends Exception>, ActionBuilder> behaviours = scenario.getBehaviour();
            Set<Class<? extends Exception>> exceptions = behaviours.keySet();
            if (exceptions.contains(null)) throw new Exception("Exception can not be null in scenario has method %s, bean %s " .formatted(action.getMethod(), action.getComponent().getClass()));
            if (!exceptions.contains(RollBackException.class)) throw new Exception("Missing default RollBackException in scenario has method %s, bean %s " .formatted(action.getMethod(), action.getComponent().getClass()));
        }
    }

    public String invoke(Object bean, String methodName, Object... args) throws Exception {
        try {
            Class<?>[] paramTypes = new Class[args.length];
            for (int i = 0; i < args.length; i++) {
                paramTypes[i] = parameterType(args[i]);
            }
            Method method = bean.getClass().getDeclaredMethod(methodName, paramTypes);
            Object result = method.invoke(bean, args);
            return result != null ? result.toString() : null;
        } catch (Exception e) {
            throw e;
        }
    }

    private static Class<?> parameterType (Object o) {
        if (o instanceof Integer) {
           return int.class;
        } else if (o instanceof Boolean) {
            return boolean.class;
        } else if (o instanceof Double) {
            return double.class;
        } else if (o instanceof Float) {
            return float.class;
        } else if (o instanceof Long) {
            return long.class;
        } else if (o instanceof Short) {
            return short.class;
        } else if (o instanceof Byte) {
            return byte.class;
        } else if (o instanceof Character) {
            return char.class;
        } else {
            return o.getClass();
        }
    }
}

3.2 Son utilisation

J'ai 3 services qui font appel à un service externe : BillingService , OrderService , PaymentService.

Service de commande

package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public String prepareOrder(String name, int number) {
        System.out.println("Prepare order for %s with order id %d ".formatted(name, number));
        return "Prepare order for %s with order id %d ".formatted(name, number);
    }

    public void Rollback_prepareOrder_NullPointException() {
        System.out.println("Rollback prepareOrder because NullPointException");
    }

    public void Rollback_prepareOrder_RollBackException() {
        System.out.println("Rollback prepareOrder because RollBackException");
    }
}

Service de facturation

package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class BillingService {

    public String prepareBilling(String name, int number) {
        System.out.println("Prepare billing for %s with order id %d ".formatted(name, number));
        return "Prepare billing for %s with order id %d ".formatted(name, number);
    }

    public String createBilling(String name, int number) {
        System.out.println("Create billing for %s with order id %d ".formatted(name, number));
        return "Create billing for %s with order id %d ".formatted(name, number);
    }

    public void Rollback_prepareBilling_NullPointException() {
        System.out.println("Rollback prepareBilling because NullPointException");
    }

    public void Rollback_prepareBilling_ArrayIndexOutOfBoundsException() {
        System.out.println("Rollback prepareBilling because ArrayIndexOutOfBoundsException");
    }

    public void Rollback_prepareBilling_RollBackException() {
        System.out.println("Rollback prepareBilling because RollBackException");
    }

    public void Rollback_createBilling_NullPointException() {
        System.out.println("Rollback createBilling because NullPointException");
    }

    public void Rollback_createBilling_ArrayIndexOutOfBoundsException() {
        System.out.println("Rollback createBilling because ArrayIndexOutOfBoundsException");
    }

    public void Rollback_createBilling_RollBackException() {
        System.out.println("Rollback createBilling because RollBackException");
    }
}

Service de paiement

package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    public String createPayment() {
        System.out.println("Create payment");
        return "Create payment";
    }

    public void Rollback_createPayment_NullPointException() {
        System.out.println("Rollback createPayment because NullPointException");
    }

    public void Rollback_createPayment_RollBackException() {
        System.out.println("Rollback createPayment because RollBackException");
    }
}

Et dans Coffee Service, je l'implémente comme suit, je crée un scénario puis je le valide.

package com.example.demo.service;

import com.example.demo.saga.DTC;
import com.example.demo.saga.exception.RollBackException;
import com.example.demo.saga.pojo.ActionBuilder;
import com.example.demo.saga.pojo.SagaItemBuilder;
import com.example.demo.saga.pojo.Scenarios;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CoffeeService {

    @Autowired
    private OrderService orderService;

    @Autowired
    private BillingService billingService;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private DTC dtc;

    public String test() throws Exception {
        Scenarios scenarios = Scenarios.builder()
                .scenario(
                        SagaItemBuilder.builder()
                                .action(ActionBuilder.builder().component(orderService).method("prepareOrder").args("tuanh.net", 123))
                                .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(orderService).method("Rollback_prepareOrder_NullPointException").args())
                                .onBehaviour(RollBackException.class, ActionBuilder.builder().component(orderService).method("Rollback_prepareOrder_RollBackException").args())
                ).scenario(
                        SagaItemBuilder.builder()
                                .action(ActionBuilder.builder().component(billingService).method("prepareBilling").args("tuanh.net", 123))
                                .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(billingService).method("Rollback_prepareBilling_NullPointException").args())
                                .onBehaviour(RollBackException.class, ActionBuilder.builder().component(billingService).method("Rollback_prepareBilling_RollBackException").args())
                ).scenario(
                         SagaItemBuilder.builder()
                                .action(ActionBuilder.builder().component(billingService).method("createBilling").args("tuanh.net", 123))
                                .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(billingService).method("Rollback_createBilling_ArrayIndexOutOfBoundsException").args())
                                .onBehaviour(RollBackException.class, ActionBuilder.builder().component(billingService).method("Rollback_createBilling_RollBackException").args())
                ).scenario(
                        SagaItemBuilder.builder()
                                .action(ActionBuilder.builder().component(paymentService).method("createPayment").args())
                                .onBehaviour(NullPointerException.class, ActionBuilder.builder().component(paymentService).method("Rollback_createPayment_NullPointException").args())
                                .onBehaviour(RollBackException.class, ActionBuilder.builder().component(paymentService).method("Rollback_createPayment_RollBackException").args())
                );
        dtc.commit(scenarios);
        return "ok";
    }
}

3.3 Résultat

Quand je fais une exception lors de la création de la facturation.

public String createBilling(String name, int number) {
    throw new NullPointerException();
}

Résultat

2024-08-24T14:21:45.445+07:00 INFO 19736 --- [demo] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2024-08-24T14:21:45.450+07:00 INFO 19736 --- [demo] [main] com.example.demo.DemoApplication : Started DemoApplication in 1.052 seconds (process running for 1.498)
2024-08-24T14:21:47.756+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-24T14:21:47.756+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2024-08-24T14:21:47.757+07:00 INFO 19736 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
Prepare order for tuanh.net with order id 123 
Prepare billing for tuanh.net with order id 123 
Rollback createBilling because RollBackException
Rollback prepareBilling because RollBackException
Rollback prepareOrder because RollBackException

Consultez mon référentiel GitHub

4. Conclusion

En résumé, le modèle Saga fournit une solution robuste pour gérer les transactions distribuées en les décomposant en étapes plus petites et gérables. Le choix entre Chorégraphie et Orchestration dépend des besoins spécifiques et de l'architecture de votre système. La chorégraphie offre un couplage lâche et une résilience, tandis que l'orchestration offre un contrôle centralisé et une surveillance plus facile. En concevant soigneusement votre système avec le modèle Saga, vous pouvez obtenir cohérence, disponibilité et flexibilité dans votre architecture de microservices distribués.

N'hésitez pas à commenter ci-dessous si vous avez des questions ou si vous avez besoin de précisions supplémentaires sur la mise en œuvre du modèle Saga dans votre système !

Lisez plus d'articles sur : Comment le modèle Saga résout les problèmes de transactions distribuées : méthodes et exemples concrets

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:
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