Saga パターンが分散トランザクションの問題を解決する方法: メソッドと実際の例

Linda Hamilton
リリース: 2024-10-20 20:11:02
オリジナル
485 人が閲覧しました

1. 問題の理解: 分散トランザクションの複雑さ

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

分散トランザクションには複数のマイクロサービスが関与し、各サービスがトランザクションの一部を実行します。たとえば、e コマース プラットフォームには、支払い、在庫、注文管理などのサービスが含まれる場合があります。トランザクションを完了するには、これらのサービスが連携する必要があります。しかし、これらのサービスの 1 つが失敗した場合はどうなるでしょうか?

1.1 現実世界のシナリオ

注文中に次のステップが発生する電子商取引アプリケーションを想像してください:

  • ステップ 1 : お客様の口座から支払いを差し引きます。
  • ステップ 2 : 在庫内のアイテム数を減らします。
  • ステップ 3 : 注文管理システムで注文を作成します。

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

支払いが差し引かれた後、注文が作成される前に在庫サービスが失敗すると、システムは一貫性のない状態になります。顧客は料金を請求されますが、注文は行われません。

1.2 従来のソリューションとその限界

このような障害に対処するには、2 フェーズ コミット プロトコルを使用した分散トランザクションの使用を検討することもできます。ただし、これによりいくつかの問題が発生します:

  • 高遅延 : 各サービスはトランザクション中にリソースをロックする必要があるため、遅延が増加します。
  • 可用性の低下 : いずれかのサービスが失敗すると、トランザクション全体がロールバックされ、システム全体の可用性が低下します。
  • 密結合 : サービスが密結合になるため、個々のサービスの拡張や変更が困難になります。

2. Saga パターンが問題をどのように解決するか

分散システムでは、トランザクションが複数のマイクロサービスにまたがることがよくあります。すべてのサービスが正常に完了するか、まったく完了しないことを確認するのは困難です。これを処理する従来の方法 (2 フェーズ コミットによる分散トランザクションを使用する) では、高い遅延、密結合、可用性の低下などの問題により問題が発生する可能性があります。

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

Saga パターンは、より柔軟なアプローチを提供します。 Saga パターンでは、トランザクションを単一の単位として実行しようとするのではなく、トランザクションを独立して実行できる小さな独立したステップに分割します。各ステップはデータベースを更新し、次のステップをトリガーするローカル トランザクションです。ステップが失敗した場合、システムは補償アクションを実行して前のステップで行われた変更を元に戻し、システムが一貫した状態に確実に戻ることができるようにします。

2.1 サーガパターンとは何ですか?

Saga パターンは、本質的には、次々に実行される一連の小さなトランザクションです。仕組みは次のとおりです:

  • ローカル トランザクション : トランザクションに関与する各サービスは、独自のローカル トランザクションを実行します。たとえば、注文処理システムでは、あるサービスが支払いを処理し、別のサービスが在庫を処理し、さらに別のサービスが注文記録を処理する可能性があります。
  • イベントまたはメッセージの公開 : サービスはローカル トランザクションを完了した後、イベントを公開するか、そのステップが正常に完了したことを示すメッセージを送信します。たとえば、支払いが処理された後、支払いサービスは「PaymentCompleted」イベントを発行する場合があります。
  • 次のステップのトリガー : シーケンス内の次のサービスはイベントをリッスンし、イベントを受信すると、ローカル トランザクションを続行します。これは、トランザクションのすべてのステップが完了するまで続きます。
  • 補償アクション : いずれかのステップが失敗した場合、補償アクションが呼び出されます。これらのアクションは、前の手順で行われた変更を元に戻すように設計されています。たとえば、支払い後に在庫削減が失敗した場合、補償アクションによって支払いが返金されます。

2.2 サーガの種類

Saga パターンを実装するには、主に コレオグラフィーオーケストレーション の 2 つの方法があります。

2.2.1 コレオグラフィーサーガ

コレオグラフィー サーガには、中心的なコーディネーターが存在しません。代わりに、Saga に関与する各サービスはイベントをリッスンし、前のステップの結果に基づいていつ行動するかを決定します。このアプローチは分散型であり、サービスが独立して動作することを可能にします。仕組みは次のとおりです:

  • イベントベースの調整 : 各サービスは、それに関連するイベントを処理する責任があります。たとえば、支払いサービスは支払いを処理した後、「PaymentCompleted」イベントを発行します。在庫サービスはこのイベントをリッスンし、イベントを受信するとアイテム数を差し引きます。
  • 分散制御 : 中央のコーディネーターが存在しないため、各サービスは受信したイベントに基づいて次に何をすべきかを認識する必要があります。これにより、システムの柔軟性が高まりますが、すべてのサービスが操作の正しいシーケンスを確実に理解できるようにするための慎重な計画が必要です。
  • 補償アクション : サービスが何か問題が発生したことを検出すると、失敗イベントを発行できます。他のサービスはそれをリッスンして補償アクションをトリガーします。たとえば、在庫サービスが在庫を更新できない場合、「InventoryUpdateFailed」イベントが発行される可能性があり、支払いサービスはこれをリッスンして返金をトリガーします。

振り付けの利点:

  • 疎結合 : サービスは疎結合されているため、個々のサービスの拡張や変更が容易になります。
  • 復元力 : 各サービスは独立して動作するため、システムは個々のサービスでの障害に対する復元力が高くなります。

振付の課題:

  • 複雑さ : サービスの数が増えると、イベントのフローの管理と理解が複雑になる可能性があります。
  • 中央制御の欠如 : 中央コーディネーターがないと、トランザクション フロー全体の監視とデバッグが困難になる可能性があります。

2.2.2 オーケストレーションの物語

オーケストレーション サーガでは、中央のオーケストレーターがトランザクション フローを制御します。オーケストレーターはステップのシーケンスを決定し、サービス間の通信を処理します。仕組みは次のとおりです:

  • 集中制御 : オーケストレーターは各サービスにコマンドを順番に送信します。たとえば、オーケストレーターは最初に、支払いサービスに支払いを処理するように指示する場合があります。それが完了すると、インベントリ サービスにインベントリを更新するように指示します。
  • 順次実行 : 各サービスは、オーケストレーターの指示がある場合にのみタスクを実行し、ステップが正しい順序で実行されるようにします。
  • 補償ロジック : オーケストレーターは、何か問題が発生した場合に補償アクションを開始する責任もあります。たとえば、在庫の更新が失敗した場合、オーケストレーターは支払いサービスに支払いを返金するよう命令できます。

オーケストレーションの利点:

  • 集中管理 : 単一のオーケストレーターを使用すると、トランザクション フローの監視、管理、デバッグが簡単になります。
  • より単純なロジック : オーケストレーターがフローを処理するため、個々のサービスはトランザクション全体のシーケンスを認識する必要がありません。

オーケストレーションの課題:

  • 単一障害点 : オーケストレーターは、高可用性を考慮して設計されていない場合、ボトルネックまたは単一障害点になる可能性があります。
  • オーケストレーターとの緊密な結合 : サービスはオーケストレーターに依存しているため、コレオグラフィーと比較してシステムの柔軟性が低くなる可能性があります。

3. シンプルなオーケストレーションサーガパターンの実装: ステップバイステップガイド

電子商取引のシナリオを検討し、Saga パターンを使用して実装してみましょう。

コーヒー購入シナリオでは、各サービスはローカル トランザクションを表します。 Coffee Service は、この物語のオーケストレーターとして機能し、購入を完了するために他のサービスを調整します。

この物語がどのように機能するかを詳しく説明します:

  • 顧客が注文します : 顧客は注文サービスを通じて注文します。
  • Coffee Service がストーリーを開始します : Coffee Service が注文を受け取り、Saga を開始します。
  • Order Service が注文を作成します : Order Service は新しい注文を作成し、それを永続化します。
  • Billing Service がコストを計算します: Billing Service は注文の合計コストを計算し、請求レコードを作成します。
  • 支払いサービスが支払いを処理します: 支払いサービスが支払いを処理します。
  • コーヒー サービスが注文ステータスを更新します: 支払いが成功すると、コーヒー サービスは注文ステータスを「完了」に更新します。

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

3.1 取引主体

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

サガの私の実装では、各 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.2 使用方法

外部サービスを呼び出すサービスが 3 つあります: BillingServiceOrderServicePaymentService

オーダーサービス

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";
    }
}
ログイン後にコピー

3.3 結果

請求の作成で例外を作成した場合。

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 リポジトリをチェックしてください

4. 結論

要約すると、Saga パターンは、分散トランザクションをより小さく管理しやすいステップに分割することで、分散トランザクションを管理するための堅牢なソリューションを提供します。コレオグラフィーとオーケストレーションのどちらを選択するかは、システムの特定のニーズとアーキテクチャによって異なります。コレオグラフィーは疎結合と復元力を提供し、オーケストレーションは集中制御と簡単なモニタリングを提供します。 Saga パターンを使用してシステムを慎重に設計することで、分散マイクロサービス アーキテクチャの一貫性、可用性、柔軟性を実現できます。

システムへの Saga パターンの実装に関してご質問がある場合、またはさらなる説明が必要な場合は、お気軽に以下にコメントしてください。

投稿の詳細については、 をご覧ください: Saga パターンが分散トランザクションの問題を解決する方法: メソッドと実際の例

以上がSaga パターンが分散トランザクションの問題を解決する方法: メソッドと実際の例の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!