メインコンテンツまでスキップ
バージョン: 3.19New

Microservice Transaction API を使用してマイクロサービストランザクションをサポートするアプリケーションを作成する

注記

このページは英語版のページが機械翻訳されたものです。英語版との間に矛盾または不一致がある場合は、英語版を正としてください。

このチュートリアルでは、Microservice Transaction API を使用して、マイクロサービストランザクションをサポートするサンプル電子商取引アプリケーションを作成する方法について説明します。

Microservice Transaction API を使用すると、2フェーズコミットプロトコルを自分で実装することなく、複数のプロセスにまたがる単一のトランザクションを実行できます。サービスが prepare、validate、または commit を呼び出すことはなく、ScalarDB Cluster がそれらを代行します。

そのため、同じアプリケーションコードをどちらの ScalarDB Cluster デプロイパターン(共有クラスターパターンまたは分離クラスターパターン)でも実行できます。このサンプルはそれを具体的に示すために構成されています。アプリケーションコードのコピーが1つと、2セットの設定ファイルが含まれています。このページ全体でタブを使用してパターンを選択してください。選択内容はすべてのステップに引き継がれます。

注記

両方のデプロイパターンで ScalarDB Cluster ノードが実行されるため、どちらを選択してもライセンスキーが必要です。

サンプルマイクロサービスアプリケーションの概要​

サンプル電子商取引アプリケーションは、ユーザーが信用枠を使用してアイテムを注文・支払いする方法を示しています。

サンプルアプリケーションには、database-per-service パターンに基づく Customer Service と Order Service という2つのマイクロサービスがあります:

  • Customer Service は、信用枠情報、信用限度額、信用合計など、顧客情報を管理します。
  • Order Service は、注文処理の実行や注文履歴の取得など、注文操作を担当します。

各サービスには gRPC エンドポイントがあります。クライアントはエンドポイントを呼び出し、サービスも相互にエンドポイントを呼び出します。

サンプルアプリケーションで使用するデータベースは MySQL、Cassandra、および PostgreSQL です。Customer Service と Order Service は ScalarDB Cluster を通じてそれぞれ MySQL と Cassandra を使用し、PostgreSQL は Coordinator テーブルを保持します。両方のデプロイパターンで同じ3つのデータベースを使用し、各名前空間を同じデータベースに配置するため、パターン間で変わるのは ScalarDB のトポロジーのみです。

2つのデプロイパターンは、ScalarDB Cluster がサービスとデータベースの間にどのように配置されるかが異なります。

共有クラスターパターンでは、両方のサービスが1つのクラスターノードに接続し、そのノードが3つのデータベースすべてにアクセスします:

分離クラスターパターンでは、各サービスがクラスターを所有し、Transaction Coordinator がトランザクションを駆動し、Coordinator テーブルはすべてのコンポーネントがアクセスできる独自のストアに移動します:

両サイドは対称的です:それぞれがサービス、クラスター、データベースを所有します。それらの外側にあるのは共有されたもの、つまりトランザクションを駆動する Transaction Coordinator と、Transaction Coordinator および両方のクラスターが読み書きする Coordinator テーブルです。

アプリケーションコードは両方で同じです。設定のみが異なります。

注記

サンプルアプリケーションは Microservice Transaction API の使用方法を示すことに重点を置いているため、アプリケーション固有のエラー処理、認証処理、および同様の機能はサンプルアプリケーションに含まれていません。

認証と認可は、2つのデプロイパターンの違いを読みやすくするために無効にされています。このトポロジーを実際のシステムに適用する前に、サービスごとのユーザーと名前空間の権限を追加してください。詳細については、ユーザーの認証と認可を参照してください。

サービスエンドポイント​

サービスで定義されているエンドポイントは次のとおりです:

  • Customer Service

    • getCustomerInfo
    • payment
    • repayment
  • Order Service

    • placeOrder
    • getOrder
    • getOrders
注記

Customer Service には prepare、validate、commit、または rollback エンドポイントがありません。それらを必要としないことが Microservice Transaction API のポイントです。

このサンプルアプリケーションで実行できること​

サンプルアプリケーションは次の種類のトランザクションをサポートしています:

  • Customer Service の getCustomerInfo エンドポイントを通じて顧客情報を取得します。
  • Order Service の placeOrder エンドポイントと Customer Service の payment エンドポイントを通じて、信用枠を使用して注文を行います。
    • 注文のコストが顧客の信用限度額を下回っているかどうかを確認します。
    • チェックが成功した場合は、注文履歴を記録し、顧客が支払った金額を更新します。
  • Order Service の getOrder エンドポイントと Customer Service の getCustomerInfo エンドポイントを通じて、注文 ID で注文情報を取得します。
  • Order Service の getOrders エンドポイントと Customer Service の getCustomerInfo エンドポイントを通じて、顧客 ID で注文情報を取得します。
  • Customer Service の repayment エンドポイントを通じて支払いを行います。
    • 顧客が支払った金額を減らします。

placeOrder、getOrder、および getOrders はそれぞれ両方のサービスにまたがる1つのトランザクションとして実行されるため、MySQL と Cassandra にまたがります。getCustomerInfo と repayment は、直接呼び出されると Customer Service 内のみで実行されるため、代わりに通常の Transaction API を使用します。

Microservice Transaction API の仕組み​

このセクションでは API 自体について説明します。これは両方のデプロイパターンに適用されます。完全な API リファレンスについては、ScalarDB Cluster Java API ガイドを参照してください。

この API はサービスにまたがるトランザクションにのみ使用してください。1つのサービス内に留まるトランザクションはプロセス間で調整するものがないため、代わりに通常の Transaction API (DistributedTransactionManager) を使用します。両方のマネージャーは同じ TransactionFactory と同じ設定ファイルから取得されます:

TransactionFactory factory = TransactionFactory.create("<CONFIGURATION_FILE_PATH>");
DistributedTransactionManager transactionManager = factory.getTransactionManager();
GlobalTransactionManager globalTransactionManager = factory.getGlobalTransactionManager();

サービスにまたがるトランザクションには2種類のハンドルがあります:

  • begin() または beginReadOnly() から取得した GlobalTransaction。トランザクション全体の結果を駆動し、データを保持しません。
  • beginBranch(transactionId) から取得したサービスごとの BranchTransaction。そのサービスの作業の CRUD ハンドルです。

イニシエーター​

1つのサービスがトランザクションを開始し、その ID を他のサービスと共有し、結果を決定します。このサンプルでは、Order Service がイニシエーターです:

GlobalTransaction global = globalTransactionManager.begin();
BranchTransaction branch = null;
try {
branch = globalTransactionManager.beginBranch(global.getId());
// ... this service's own CRUD ...
branch.end(BranchTransaction.Status.SUCCESS);
branch = null;

callCustomerService(global.getId()); // pass the transaction ID over gRPC

global.commit();
} catch (UnknownTransactionStatusException e) {
// ... end the branch with FAILURE; do not roll back ...
} catch (Exception e) {
// ... end the branch with FAILURE, then roll back ...
}
注記

順序に注目してください:このサービスは他のサービスを呼び出す前に自身の作業を終了してブランチを終了します。リモート呼び出しをまたいでブランチを開いたままにすることも正しいですが、サービスが完了したらすぐにブランチを終了することが API が想定するイディオムです。

参加者​

トランザクション ID を受け取ったサービスは、自身のブランチで同じトランザクションに参加し、作業を行い、ブランチを終了します。コミットやロールバックは行いません—それはイニシエーターの責任です:

BranchTransaction branch = globalTransactionManager.beginBranch(transactionId);
// ... this service's own CRUD ...
branch.end(BranchTransaction.Status.SUCCESS);

このサンプルでは、Customer Service はリクエストにトランザクション ID が含まれている場合に参加者として実行されます。それ以外の場合は作業がサービス内に留まるため、Microservice Transaction API ではなく Transaction API を通じて通常のトランザクションとして実行されます。payment エンドポイントは常に参加者です。repayment は決して参加者になりません。getCustomerInfo は呼び出し元によって、どちらにもなります。

ブランチの終了は必須​

すべてのブランチは、結果にかかわらず、正確に1回終了する必要があります。その義務はブランチを実行するプロセスに属し、他のどのプロセスもそれを代行することはできません。失敗パスでは、ブランチを Status.FAILURE で終了してください:

branch.end(BranchTransaction.Status.FAILURE);

end(FAILURE) は寛容であり、catch ブロックから呼び出すことができます:すでに終了したブランチを終了することは何もしません(no-op)。元の失敗を隠すこともありません。

注記

end(Status)、commit()、および rollback() はそれぞれチェック例外を宣言するため、それらを呼び出す catch ブロックは独自の処理が必要です。

エラー処理​

イニシエーターのみがトランザクションを再開始できるため、イニシエーターのみが再試行します。このサンプルでは、Order Service が再試行ループを持ち、Customer Service も自身で実行するトランザクションに対して再試行ループを持ちます。参加者パスは再試行しません。

すべての失敗が再試行に値するわけではありません。サンプルは gRPC の NOT_FOUND および FAILED_PRECONDITION ステータスを決定論的なビジネス失敗(不明なアイテム、または超過した信用限度額)として扱い、それらを再試行しません。

UnknownTransactionStatusException は個別に処理され、再試行されません。トランザクションがコミットされたかどうかが不明であることを意味します。再試行するとコミット済みトランザクションが重複する可能性があります。実際のステータスを特定することはアプリケーションの責任です。

前提条件​

警告

ScalarDB Cluster を使用するには、ライセンスキー (試用ライセンスまたは商用ライセンス) が必要です。ライセンスキーをお持ちでない場合は、試用ライセンスキーの設定方法に従ってすぐに使える試用ライセンスキーをご利用いただけます。商用ライセンスを取得するには、お問い合わせください。

ScalarDB Cluster のセットアップ​

次のセクションでは、ScalarDB Cluster で Microservice Transaction API を使用してマイクロサービストランザクションをサポートするサンプルアプリケーションをセットアップする方法について説明します。

ScalarDB サンプルリポジトリをクローンする​

ターミナルを開き、次のコマンドを実行して ScalarDB サンプルリポジトリをクローンします:

git clone https://github.com/scalar-labs/scalardb-samples

次のコマンドを実行して、サンプルアプリケーションが含まれているディレクトリに移動します:

cd scalardb-samples/microservice-transaction-sample-with-cluster/

デプロイパターンの選択​

サンプルにはアプリケーションコードのコピーが1つと、2つの設定ディレクトリが含まれています:

  • shared-cluster/: 両方のサービスが1つの ScalarDB Cluster ノードに接続し、そのノードが両方の名前空間と Coordinator テーブルを保持します。
  • separated-clusters/: 各サービスが ScalarDB Cluster ノードを所有し、Transaction Coordinator がそれらにまたがる2フェーズコミットを駆動します。Coordinator テーブルはパターン内のすべての ScalarDB コンポーネントがアクセスできる独自のストアに配置されます。

パターンの選択に関するガイダンスについては、マイクロサービスにおける ScalarDB Cluster のデプロイパターンを参照してください。

以下でパターンを選択してください。選択内容はこのページのすべてのステップに適用されます。

このチュートリアルのすべてのコマンドは shared-cluster ディレクトリから実行します:

cd shared-cluster/

ライセンスキーの設定​

各 ScalarDB Cluster ノードは起動時にライセンスキーを確認するため、選択したパターンで実行されるすべてのノードのプロパティファイルに設定する必要があります。

scalardb-cluster-node.properties に ScalarDB Cluster のライセンスキーと証明書を設定します:

scalar.db.cluster.node.licensing.license_key=<YOUR_LICENSE_KEY>
scalar.db.cluster.node.licensing.license_check_cert_pem=<YOUR_LICENSE_CHECK_CERT_PEM>

詳細については、製品ライセンスキーの設定方法を参照してください。

注記

このサンプルアプリケーションを素早くセットアップするために、ScalarDB Cluster はスタンドアロンモードで実行され、ワイヤ暗号化は有効になっていません。本番環境では、ワイヤ暗号化を有効にすることをお勧めします。詳細については、ScalarDB Cluster スタンドアロンモードとワイヤ通信の暗号化を参照してください。

ScalarDB Cluster の設定​

両方のパターンのプロパティファイルはサンプルアプリケーション用にすでに設定されているため、このセクションと次のセクションでは、上記で設定したライセンスキー以外に変更する必要はありません。両セクションでは設定の内容を説明します。

クラスター側の設定は、2つのパターンが構造的に異なる部分です。続くマイクロサービスの設定は、主にここでの設定を反映しています。

1つのノードが両方のサービスにサービスを提供するため、両方の名前空間と Coordinator テーブルにアクセスする必要があります。scalardb-cluster-node.properties で MySQL、Cassandra、PostgreSQL にアクセスするためにマルチストレージを使用します:

scalar.db.storage=multi-storage
scalar.db.multi_storage.storages=mysql,cassandra,coordinatordb

scalar.db.multi_storage.storages.mysql.storage=jdbc
scalar.db.multi_storage.storages.mysql.contact_points=jdbc:mysql://mysql:3306/?sslMode=REQUIRED
scalar.db.multi_storage.storages.mysql.username=root
scalar.db.multi_storage.storages.mysql.password=mysql

scalar.db.multi_storage.storages.cassandra.storage=cassandra
scalar.db.multi_storage.storages.cassandra.contact_points=cassandra
scalar.db.multi_storage.storages.cassandra.username=cassandra
scalar.db.multi_storage.storages.cassandra.password=cassandra

scalar.db.multi_storage.storages.coordinatordb.storage=jdbc
scalar.db.multi_storage.storages.coordinatordb.contact_points=jdbc:postgresql://postgres:5432/postgres
scalar.db.multi_storage.storages.coordinatordb.username=postgres
scalar.db.multi_storage.storages.coordinatordb.password=postgres

scalar.db.multi_storage.namespace_mapping=customer_service:mysql,coordinator:coordinatordb
scalar.db.multi_storage.default_storage=cassandra

scalar.db.cluster.node.standalone_mode.enabled=true

namespace_mapping は各名前空間をストレージに送信し、default_storage はリストにない名前空間をキャッチします。これが order_service にエントリが必要ない理由です。Coordinator テーブルはここでも、もう一方のパターンでも独自のデータベースを取得し、各名前空間が両方のパターンで同じデータベースに配置され、トポロジーのみが異なります。

standalone_mode.enabled=true はノードを静的メンバーシップを持つ単一インスタンスとして実行します。このキーがない場合、ノードは Kubernetes を通じてピアを検出しようとします。

マルチストレージの詳細については、マルチストレージトランザクションをサポートするように ScalarDB を設定する方法を参照してください。スタンドアロンモードの詳細については、ScalarDB Cluster スタンドアロンモードを参照してください。

マイクロサービスの設定​

ここが2つのパターンが実際に異なる部分です。アプリケーションコードは同一です。これらのプロパティのみが変わります。

両方のサービスが同じクラスターノードに接続し、そのノードがトランザクションを保持します。Transaction Coordinator は関与しないため、customer-service.properties と order-service.properties は同一です:

scalar.db.transaction_manager=cluster
scalar.db.contact_points=indirect:scalardb-cluster-node
scalar.db.cluster.client.transaction_coordinator.enabled=false

transaction_coordinator.enabled=false はデフォルトであり、省略することもできます。もう一方のパターンとの違いがファイル内で明示的に見えるように記述されています。

データベースと ScalarDB Cluster の起動​

各サービスは起動時に ScalarDB Cluster に接続するため、マイクロサービスの前にインフラストラクチャを起動します。コンテナの数はパターンによって異なります。

MySQL、Cassandra、PostgreSQL、および1つの ScalarDB Cluster ノードを起動します:

docker compose up -d mysql cassandra postgres scalardb-cluster-node

クラスターノードはマルチストレージ設定を使用します:MySQL は customer_service 名前空間を保持し、Cassandra は order_service を保持し、PostgreSQL は Coordinator テーブルを保持します。詳細については、マルチストレージトランザクションをサポートするように ScalarDB を設定する方法を参照してください。

注記

開発環境によっては、Docker コンテナの起動に1分以上かかる場合があります。

スキーマのロード​

スキーマは選択したディレクトリの JSON ファイルに定義されています。スキーマを適用するには、ScalarDB のリリースページに移動し、使用する ScalarDB Cluster のバージョンに対応する ScalarDB Cluster Schema Loader (scalardb-cluster-schema-loader-<X.Y.Z>-all.jar) をダウンロードします。

次のコマンドを実行します。<X.Y.Z> はダウンロードしたバージョンに置き換えてください。

1つのクラスターがすべてを保持するため、1回の実行で両方のサービスのテーブルと Coordinator テーブルが適用されます:

java -jar scalardb-cluster-schema-loader-<X.Y.Z>-all.jar \
--config scalardb-cluster-node-client.properties \
--schema-file schema.json --coordinator

--coordinator オプションは必須です。Coordinator テーブルは JSON スキーマファイルで宣言できません。

スキーマの詳細​

Customer Service のすべてのテーブルは customer_service 名前空間に作成されます:

  • customer_service.customers: 顧客情報を管理するテーブル
    • credit_limit: 貸し手が各顧客に信用枠の使用を許可する最大金額
    • credit_total: 各顧客が信用枠を使用してすでに使用した金額

Order Service のすべてのテーブルは order_service 名前空間に作成されます:

  • order_service.orders: 注文情報を管理するテーブル
  • order_service.statements: 注文明細情報を管理するテーブル
  • order_service.items: 注文するアイテムの情報を管理するテーブル

スキーマのエンティティリレーションシップダイアグラムは次のとおりです:

ERD

マイクロサービスの起動​

2つのサービスの Docker イメージをビルドして起動します。Gradle ビルドはサンプルのルートに定義されているため、-p .. はパターンのディレクトリにいる間に Gradle をそこに向けます:

../gradlew -p .. :customer-service:docker :order-service:docker
docker compose up -d customer-service order-service

イメージには設定が含まれていません。各パターンの docker-compose.yml が必要なプロパティファイルをマウントするため、同じイメージタグが両方のパターンに対して実行されます。イメージがパターン間で同じであることを確認できます:

docker image inspect --format '{{.Id}}' sample-mstx-order-service:1.0

各サービスは起動時に独自の初期データをロードするため、別途のデータロードステップは不要です。Customer Service は3人の顧客をシードし、Order Service は注文できる5つのアイテムをシードします。

サンプルアプリケーションでトランザクションを実行し、データを取得する​

次のセクションでは、サンプル電子商取引アプリケーションでトランザクションを実行し、データを取得する方法について説明します。コマンドとその結果は両方のデプロイパターンで同じです。

次のコマンドを実行して、サンプルのルートディレクトリに戻ります:

cd ..

顧客情報を取得する​

次のコマンドを実行して、ID が 1 である顧客に関する情報を取得することから始めます:

./gradlew :client:run --args="GetCustomerInfo 1" -q

次の出力が表示されます:

{
"id": 1,
"name": "Yamada Taro",
"creditLimit": 10000
}

この時点では creditTotal は表示されていません。これは creditTotal の現在の値が 0 であることを意味します。

注文する​

次に、次のコマンドを実行して、顧客 ID 1 にリンゴ2個とオレンジ1個を注文してもらいます:

注記

このコマンドの注文形式は ./gradlew :client:run --args="PlaceOrder <CUSTOMER_ID> <ITEM_ID>:<COUNT>,<ITEM_ID>:<COUNT>,..." です。

./gradlew :client:run --args="PlaceOrder 1 1:2,2:1" -q

以下のように、orderId の UUID が異なる、注文が成功したことを示す出力が表示されます:

{
"orderId": "bcb15594-9a0a-449e-ba20-e1dfed9e6e1e"
}

この1つのコマンドは両方のサービスにまたがるトランザクションを実行しました:Order Service が注文とその明細を記録し、Customer Service が顧客の信用枠に課金しました。どちらも発生したか、どちらも発生しなかったかのどちらかです。

注文の詳細を確認する​

次のコマンドを実行して注文の詳細を確認します。<ORDER_ID_UUID> は、前のコマンドを実行した後に表示される orderId の UUID に置き換えます:

./gradlew :client:run --args="GetOrder <ORDER_ID_UUID>" -q

orderId の UUID と timestamp の値が異なる、以下のような出力が表示されます:

{
"order": {
"orderId": "bcb15594-9a0a-449e-ba20-e1dfed9e6e1e",
"timestamp": "1788401092031",
"customerId": 1,
"customerName": "Yamada Taro",
"statement": [{
"itemId": 1,
"itemName": "Apple",
"price": 1000,
"count": 2,
"total": 2000
}, {
"itemId": 2,
"itemName": "Orange",
"price": 2000,
"count": 1,
"total": 2000
}],
"total": 4000
}
}

customerName は Customer Service からのものです。これは読み取り専用のトランザクション(Order Service が beginReadOnly() で開始)で、それでも両方のサービスにまたがっています。

クレジット合計の確認​

次のコマンドを実行して、顧客のクレジット合計を確認します:

./gradlew :client:run --args="GetCustomerInfo 1" -q

次の出力が表示されます。これは、顧客 ID 1 がクレジットの 4000 を使用したことを示しています:

{
"id": 1,
"name": "Yamada Taro",
"creditLimit": 10000,
"creditTotal": 4000
}

トランザクションのロールバックを確認する​

次に、顧客の残りのクレジットを超える注文を試みます。10000 の限度額のうち既に 4000 を使用している状態で、単価 3000 のメロンを3個注文すると、代金は 9000 になります:

./gradlew :client:run --args="PlaceOrder 1 5:3" -q

コマンドは FAILED_PRECONDITION: Credit limit exceeded で失敗します。その後、両方のサービスを確認します:

./gradlew :client:run --args="GetCustomerInfo 1" -q
./gradlew :client:run --args="GetOrders 1" -q

creditTotal はまだ 4000 で、新しい注文は表示されません。Customer Service が支払いを拒否し、Order Service の注文の書き込みはロールバックされました。その書き込みが自身のブランチでは既に成功していたにもかかわらずです。どちらのサービスもそのいずれも実装していません。

支払いを行う​

次のコマンドを実行して支払いを行います:

./gradlew :client:run --args="Repayment 1 1000" -q

次に、クレジット合計を再度確認します:

./gradlew :client:run --args="GetCustomerInfo 1" -q

creditTotal が 3000 に減ったことが確認できます。このトランザクションは Customer Service のみに関与するため、マイクロサービストランザクションとしてではなく、Transaction API を通じて通常のトランザクションとして実行されます。

サンプルアプリケーションを停止する​

サンプルアプリケーションを停止するには、選択したパターンのディレクトリに戻り、コンテナを停止します:

cd shared-cluster/
docker compose down -v
注記

もう一方のデプロイパターンを試す場合は、他のパターンを起動する前に現在のパターンを上記の方法で停止してください。2つのパターンは同じコンテナ名とホストポートを使用し、Docker Compose は2つのディレクトリを別々のプロジェクトとして扱うため、他のパターンのコンテナを自動的に停止しません。

参照​