分散トランザクションには複数のマイクロサービスが関与し、各サービスがトランザクションの一部を実行します。たとえば、e コマース プラットフォームには、支払い、在庫、注文管理などのサービスが含まれる場合があります。トランザクションを完了するには、これらのサービスが連携する必要があります。しかし、これらのサービスの 1 つが失敗した場合はどうなるでしょうか?
注文中に次のステップが発生する電子商取引アプリケーションを想像してください:
支払いが差し引かれた後、注文が作成される前に在庫サービスが失敗すると、システムは一貫性のない状態になります。顧客は料金を請求されますが、注文は行われません。
このような障害に対処するには、2 フェーズ コミット プロトコルを使用した分散トランザクションの使用を検討することもできます。ただし、これによりいくつかの問題が発生します:
分散システムでは、トランザクションが複数のマイクロサービスにまたがることがよくあります。すべてのサービスが正常に完了するか、まったく完了しないことを確認するのは困難です。これを処理する従来の方法 (2 フェーズ コミットによる分散トランザクションを使用する) では、高い遅延、密結合、可用性の低下などの問題により問題が発生する可能性があります。
Saga パターンは、より柔軟なアプローチを提供します。 Saga パターンでは、トランザクションを単一の単位として実行しようとするのではなく、トランザクションを独立して実行できる小さな独立したステップに分割します。各ステップはデータベースを更新し、次のステップをトリガーするローカル トランザクションです。ステップが失敗した場合、システムは補償アクションを実行して前のステップで行われた変更を元に戻し、システムが一貫した状態に確実に戻ることができるようにします。
Saga パターンは、本質的には、次々に実行される一連の小さなトランザクションです。仕組みは次のとおりです:
Saga パターンを実装するには、主に コレオグラフィー と オーケストレーション の 2 つの方法があります。
コレオグラフィー サーガには、中心的なコーディネーターが存在しません。代わりに、Saga に関与する各サービスはイベントをリッスンし、前のステップの結果に基づいていつ行動するかを決定します。このアプローチは分散型であり、サービスが独立して動作することを可能にします。仕組みは次のとおりです:
振り付けの利点:
振付の課題:
オーケストレーション サーガでは、中央のオーケストレーターがトランザクション フローを制御します。オーケストレーターはステップのシーケンスを決定し、サービス間の通信を処理します。仕組みは次のとおりです:
オーケストレーションの利点:
オーケストレーションの課題:
電子商取引のシナリオを検討し、Saga パターンを使用して実装してみましょう。
コーヒー購入シナリオでは、各サービスはローカル トランザクションを表します。 Coffee Service は、この物語のオーケストレーターとして機能し、購入を完了するために他のサービスを調整します。
この物語がどのように機能するかを詳しく説明します:
サガの私の実装では、各 SagaItemBuilder は分散トランザクション フローのステップを表します。 ActionBuilder は、メイン アクションと、エラーが発生した場合に実行されるロールバック アクションなど、実行されるアクションを定義します。 ActionBuilder は、次の 3 つの情報をカプセル化します:
component : 呼び出されるメソッドが存在する Bean インスタンス。
method : 呼び出されるメソッドの名前。
args : メソッドに渡される引数。
アクションビルダー
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; } }
シナリオ
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; } }
以下は、配布トランザクションをコミットする方法です。
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 つあります: BillingService、OrderService、PaymentService。
オーダーサービス
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"); } }
BillingService
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"); } }
支払いサービス
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"); } }
Coffee Service では、次のように実装し、シナリオを作成してコミットします。
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"; } }
請求の作成で例外を作成した場合。
public String createBilling(String name, int number) { throw new NullPointerException(); }
結果
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
GitHub リポジトリをチェックしてください
要約すると、Saga パターンは、分散トランザクションをより小さく管理しやすいステップに分割することで、分散トランザクションを管理するための堅牢なソリューションを提供します。コレオグラフィーとオーケストレーションのどちらを選択するかは、システムの特定のニーズとアーキテクチャによって異なります。コレオグラフィーは疎結合と復元力を提供し、オーケストレーションは集中制御と簡単なモニタリングを提供します。 Saga パターンを使用してシステムを慎重に設計することで、分散マイクロサービス アーキテクチャの一貫性、可用性、柔軟性を実現できます。
システムへの Saga パターンの実装に関してご質問がある場合、またはさらなる説明が必要な場合は、お気軽に以下にコメントしてください。
投稿の詳細については、 をご覧ください: Saga パターンが分散トランザクションの問題を解決する方法: メソッドと実際の例
以上がSaga パターンが分散トランザクションの問題を解決する方法: メソッドと実際の例の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。