Verteilte Transaktionen umfassen mehrere Mikrodienste, wobei jeder Dienst einen Teil einer Transaktion ausführt. Beispielsweise könnte eine E-Commerce-Plattform Dienste wie Zahlung, Inventar und Auftragsverwaltung umfassen. Diese Dienste müssen zusammenarbeiten, um eine Transaktion abzuschließen. Was passiert jedoch, wenn einer dieser Dienste ausfällt?
Stellen Sie sich eine E-Commerce-Anwendung vor, bei der während einer Bestellung die folgenden Schritte ausgeführt werden:
Wenn der Inventardienst nach dem Abzug der Zahlung, aber vor der Erstellung der Bestellung fehlschlägt, befindet sich das System in einem inkonsistenten Zustand. Dem Kunden wird eine Gebühr berechnet, es wird jedoch keine Bestellung aufgegeben.
Um solche Fehler zu bewältigen, könnte man die Verwendung einer verteilten Transaktion mit einem zweiphasigen Commit-Protokoll in Betracht ziehen. Dies führt jedoch zu mehreren Problemen:
In verteilten Systemen erstrecken sich Transaktionen oft über mehrere Microservices. Es ist eine Herausforderung sicherzustellen, dass alle Dienste entweder erfolgreich oder gar nicht abgeschlossen werden. Die herkömmliche Vorgehensweise – die Verwendung verteilter Transaktionen mit zweiphasigem Commit – kann aufgrund von Problemen wie hoher Latenz, enger Kopplung und verringerter Verfügbarkeit problematisch sein.
Das Saga-Muster bietet einen flexibleren Ansatz. Anstatt zu versuchen, eine Transaktion als einzelne Einheit auszuführen, zerlegt das Saga-Muster die Transaktion in kleinere, isolierte Schritte, die unabhängig voneinander ausgeführt werden können. Jeder Schritt ist eine lokale Transaktion, die die Datenbank aktualisiert und dann den nächsten Schritt auslöst. Wenn ein Schritt fehlschlägt, führt das System Ausgleichsmaßnahmen durch, um die in den vorherigen Schritten vorgenommenen Änderungen rückgängig zu machen und sicherzustellen, dass das System zu einem konsistenten Zustand zurückkehren kann.
Das Saga-Muster ist im Wesentlichen eine Folge kleinerer Transaktionen, die nacheinander ausgeführt werden. So funktioniert es:
Es gibt im Wesentlichen zwei Möglichkeiten, das Saga-Muster umzusetzen: Choreographie und Orchestrierung.
In einer Choreographie-Saga gibt es keinen zentralen Koordinator. Stattdessen wartet jeder an der Saga beteiligte Dienst auf Ereignisse und entscheidet auf der Grundlage des Ergebnisses der vorherigen Schritte, wann er handelt. Dieser Ansatz ist dezentral und ermöglicht den unabhängigen Betrieb der Dienste. So funktioniert es:
Vorteile der Choreographie:
Herausforderungen der Choreographie:
In einer Orchestration Saga steuert ein zentraler Orchestrator den Ablauf der Transaktion. Der Orchestrator bestimmt die Reihenfolge der Schritte und kümmert sich um die Kommunikation zwischen Diensten. So funktioniert es:
Vorteile der Orchestrierung:
Herausforderungen der Orchestrierung:
Betrachten wir das E-Commerce-Szenario und implementieren es mithilfe des Saga-Musters.
In unserem Kaffeeeinkaufsszenario stellt jede Dienstleistung eine lokale Transaktion dar. Der Coffee Service fungiert als Orchestrator dieser Saga und koordiniert die anderen Dienste, um den Kauf abzuschließen.
Hier ist eine Aufschlüsselung, wie die Saga funktionieren könnte:
In meiner Implementierung der Saga stellt jeder SagaItemBuilder einen Schritt in unserem verteilten Transaktionsfluss dar. Der ActionBuilder definiert die auszuführenden Aktionen, einschließlich der Hauptaktion und der Rollback-Aktion, die ausgeführt wird, wenn ein Fehler auftritt. Der ActionBuilder kapselt drei Informationen:
component : Die Bean-Instanz, in der sich die aufzurufende Methode befindet.
Methode: Der Name der aufzurufenden Methode.
args: Die Argumente, die an die Methode übergeben werden sollen.
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; } }
Szenarien
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; } }
Unten erfahren Sie, wie ich die Verteilungstransaktion festschreiben kann.
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(); } } }
Ich habe 3 Dienste, die externe Dienste aufrufen: BillingService, OrderService, PaymentService.
Bestellservice
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"); } }
Abrechnungsservice
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"); } }
Zahlungsservice
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"); } }
Und im Coffee Service setze ich es wie folgt um: Ich erstelle ein Szenario und schreibe es dann fest.
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"; } }
Wenn ich beim Erstellen einer Abrechnung eine Ausnahme mache.
public String createBilling(String name, int number) { throw new NullPointerException(); }
Ergebnis
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
Schauen Sie sich mein GitHub-Repository an
Zusammenfassend lässt sich sagen, dass das Saga-Muster eine robuste Lösung für die Verwaltung verteilter Transaktionen bietet, indem es diese in kleinere, überschaubare Schritte unterteilt. Die Wahl zwischen Choreografie und Orchestrierung hängt von den spezifischen Anforderungen und der Architektur Ihres Systems ab. Choreografie bietet lose Kopplung und Ausfallsicherheit, während Orchestrierung eine zentrale Steuerung und einfachere Überwachung ermöglicht. Indem Sie Ihr System sorgfältig mit dem Saga-Muster entwerfen, können Sie Konsistenz, Verfügbarkeit und Flexibilität in Ihrer verteilten Microservices-Architektur erreichen.
Kommentieren Sie unten, wenn Sie Fragen haben oder weitere Erläuterungen zur Implementierung des Saga-Musters in Ihrem System benötigen!
Weitere Beiträge finden Sie unter: Wie das Saga-Muster verteilte Transaktionsprobleme löst: Methoden und Beispiele aus der Praxis
Das obige ist der detaillierte Inhalt vonWie das Saga-Muster verteilte Transaktionsprobleme löst: Methoden und Beispiele aus der Praxis. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!