JavaにおけるSagaパターン:分散システムにおける長時間トランザクションの習得
Sagaデザインパターンの目的
複数のサービスにわたる分散トランザクションを、耐障害性と信頼性の高い方法で管理および調整するため。
現実世界の例を用いたSagaパターンの詳細な説明
現実世界の例
旅行代理店が顧客のバケーションパッケージを調整しているとします。パッケージには、フライトの予約、ホテルの部屋の予約、レンタカーが含まれています。これらの予約はそれぞれ異なるサービスプロバイダーによって管理されています。フライトの予約は成功したがホテルが満室の場合、代理店はフライトをキャンセルして顧客に通知する必要があります。これにより、顧客が不完全なバケーションパッケージを受け取ることを防ぎます。Sagaパターンでは、このシナリオは一連の調整されたトランザクションによって管理され、整合性を維持するための補償アクション(フライトのキャンセルなど)が実行されます。
簡単な言葉で
JavaにおけるSagaパターンは、データの整合性とフォールトトレランスを確保するために、一連のイベントと補償アクションを使用してマイクロサービス間の分散トランザクションを調整します。
Wikipediaによると
長時間トランザクション(Sagaインタラクションパターンとも呼ばれる)は、非ローカルリソースのロックを回避し、補償を使用して障害を処理し、潜在的に小さなACIDトランザクション(アトミックトランザクションとも呼ばれる)を集めて、通常はコーディネーターを使用してトランザクションを完了または中止するコンピューターデータベーストランザクションです。ACIDトランザクションのロールバックとは対照的に、補償は元の状態、またはそれと同等の状態を復元し、ビジネス固有です。たとえば、ホテルの予約を行うための補償アクションは、その予約をキャンセルすることです。
JavaにおけるSagaパターンのプログラム例
Sagaデザインパターンは、各トランザクションが単一サービス内のデータを更新する一連のローカルトランザクションです。各サービスが独自のデータベースを持つマイクロサービスアーキテクチャで特に役立ちます。Sagaパターンは、サービス間のデータの整合性とフォールトトレランスを保証します。Sagaパターンの主要コンポーネントは次のとおりです。
**Saga**: Sagaは一連のローカルトランザクションであり、それぞれがチャプターと呼ばれます。Sagaはこれらのトランザクションのシーケンスを管理し、各トランザクションが正しい順序で実行され、トランザクションが失敗した場合にSagaがロールバックされることを保証します。
**Chapter**: Sagaの各チャプターはローカルトランザクションを表します。チャプターには、名前、結果(`INIT`、`SUCCESS`、または`ROLLBACK`)、および入力値があります。`Chapter`クラスは、これらのプロパティを取得および設定するためのメソッドを提供します。
**Service**: サービスはローカルトランザクションを実行します。チャプターの入力値を処理し、`ChapterResult`を返します。トランザクションが失敗した場合、チャプターのステータスを`ROLLBACK`に設定します。
**Service Discovery**: このコンポーネントは、利用可能なサービスを検出し、Sagaを実行する役割を担います。Sagaの各チャプターを順番に処理します。チャプターが失敗すると、Sagaはロールバックされます。
**Saga Result**: Sagaの結果は、`PROGRESS`、`FINISHED`、または`ROLLBACKED`です。これは、`Saga`クラスの`getResult`メソッドによって決定されます。
実際のアプリケーションでは、`Service`クラスにはローカルトランザクションを実行し、障害を処理するロジックが含まれます。`Saga`クラスは、ローカルトランザクションのシーケンスを管理し、各トランザクションが正しい順序で実行され、トランザクションが失敗した場合にSagaがロールバックされることを保証します。
スニペット1:Sagaの作成
Sagaパターンを使用する最初のステップは、Sagaを作成することです。Sagaは一連のチャプターであり、それぞれがローカルトランザクションを表します。`Saga`クラスは、チャプターを追加したり、チャプターが存在するかどうかを確認したりするためのメソッドを提供します。
// Create a new Saga
Saga saga = Saga.create();
スニペット2:Sagaへのチャプターの追加
Sagaの各チャプターはローカルトランザクションを表します。`chapter`メソッドを使用して、Sagaにチャプターを追加できます。
// Add chapters to the Saga
saga.chapter("init an order");
saga.chapter("booking a Fly");
saga.chapter("booking a Hotel");
saga.chapter("withdrawing Money");
スニペット3:チャプターの入力値の設定
Sagaの各チャプターには入力値を設定できます。`setInValue`メソッドを使用して、最後に追加されたチャプターの入力値を設定できます。
// Set input values for the chapters
saga.chapter("init an order").setInValue("good_order");
スニペット4:Sagaの実行
サービスを使用してSagaを実行できます。サービスは、Sagaの各チャプターを順番に処理します。チャプターが失敗すると、Sagaはロールバックされます。
// Execute the Saga
var service = sd.findAny();
var goodOrderSaga = service.execute(saga);
スニペット5:Sagaの結果の確認
`getResult`メソッドを使用して、Sagaの結果を確認できます。このメソッドは、Sagaの結果(`PROGRESS`、`FINISHED`、または`ROLLBACKED`)を返します。
// Check the result of the Saga
SagaResult result = goodOrderSaga.getResult();
`SagaApplication`クラスには、例を実行するための`main`メソッドがあります。
@Slf4j
public class SagaApplication {
public static void main(String[] args) {
var sd = serviceDiscovery();
var service = sd.findAny();
var goodOrderSaga = service.execute(newSaga("good_order"));
var badOrderSaga = service.execute(newSaga("bad_order"));
LOGGER.info("orders: goodOrder is {}, badOrder is {}",
goodOrderSaga.getResult(), badOrderSaga.getResult());
}
private static Saga newSaga(Object value) {
return Saga
.create()
.chapter("init an order").setInValue(value)
.chapter("booking a Fly")
.chapter("booking a Hotel")
.chapter("withdrawing Money");
}
private static ServiceDiscoveryService serviceDiscovery() {
var sd = new ServiceDiscoveryService();
return sd
.discover(new OrderService(sd))
.discover(new FlyBookingService(sd))
.discover(new HotelBookingService(sd))
.discover(new WithdrawMoneyService(sd));
}
}
- **Saga**: `SagaApplication`は、`Saga.create()`メソッドを使用して新しいSagaを作成します。次に、`chapter`メソッドを使用してSagaにチャプターを追加し、`setInValue`メソッドを使用して各チャプターの入力値を設定します。
- **Service**: `SagaApplication`は、サービスを使用してSagaのチャプターを実行します。各サービスはローカルトランザクションを表します。`SagaApplication`は、`ServiceDiscoveryService`を使用して利用可能なサービスを検出し、Sagaを実行します。
- **Service Discovery**: `ServiceDiscoveryService`は、利用可能なサービスを検出するために使用されます。`SagaApplication`はこれを使用してサービスを見つけ、Sagaを実行します。
- **Saga Execution**: `SagaApplication`は、サービスの`execute`メソッドを使用してSagaを実行します。正常な注文と不正な注文の2つのSagaを作成し、それらを実行します。
- **Saga Result**: `SagaApplication`は、`getResult`メソッドを使用してSagaの結果を確認します。正常な注文Sagaと不正な注文Sagaの結果をログに記録します。
要約すると、`SagaApplication`はSagaを作成し、チャプターを追加し、各チャプターの入力値を設定し、サービスを検出し、サービスを使用してSagaを実行し、Sagaの結果を確認します。
例を実行すると、次のコンソール出力が生成されます。
11:32:17.779 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'init an order' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Fly' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Hotel' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'withdrawing Money' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- the saga has been finished with FINISHED status
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'init an order' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Fly' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Hotel' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'withdrawing Money' has been started. But the exception has been raised.The rollback is about to start
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'booking a Hotel' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'booking a Fly' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'init an order' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- the saga has been finished with ROLLBACKED status
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.SagaApplication -- orders: goodOrder is FINISHED, badOrder is ROLLBACKED
これは、Sagaデザインパターンの使用方法の基本的な例です。実際のアプリケーションでは、`Saga`クラスは、ローカルトランザクションのシーケンスを管理し、各トランザクションが正しい順序で実行され、トランザクションが失敗した場合にSagaがロールバックされることを保証します。
JavaでSagaパターンを使用する場合
- 複数のマイクロサービスにまたがる複雑なトランザクションがある場合。
- 従来の2フェーズコミットを使用せずに、サービス間のデータの整合性を確保する必要がある場合。
- 長時間トランザクションを非同期で処理する必要がある場合。
JavaにおけるSagaパターンの実際のアプリケーション
- 注文、在庫、決済サービスを管理するeコマースプラットフォーム。
- 複数のサービスにわたる口座の借方と貸方を調整する銀行システム。
- フライト、ホテル、レンタカーを調整する旅行予約システム。
Sagaパターンの利点とトレードオフ
利点
- フォールトトレランスと信頼性の向上。
- 分離されたサービスによるスケーラビリティ。
- 長時間トランザクションを処理する際の柔軟性。
トレードオフ
- 補償トランザクションの処理の複雑さの増加。
- 部分的な障害とロールバックのシナリオを処理するための注意深い設計が必要。
- 非同期性による潜在的なレイテンシ。
関連するJavaデザインパターン
- イベントソーシング:状態の変更を一連のイベントとしてキャプチャするために使用され、状態の変更履歴を提供することでSagaパターンを補完できます。
- コマンドクエリ責務分離(CQRS):Sagaパターンと組み合わせて使用して、コマンドとクエリの責務を分離し、スケーラビリティとフォールトトレランスを向上させることができます。