2025-05-26

分散トランザクションを実装してみる

1つのDBであればORMのPrismaを使えばトランザクション実行することができますが、複数のDB間でのトランザクションはそうはいきません。 先日別々なDB間でのトランザクションを初めてSagaパターンを用いて分散トランザクションを実装したのでその時に調べたことや実装方法をまとめます。

Sagaパターンとは?

Sagaパターンは、複数のサービスやデータベースにまたがるトランザクションを管理するための設計パターンです。 従来の2フェーズコミット(2PC)と異なり、Sagaパターンは各サービスで独立したトランザクションを実行し、失敗した場合には補償トランザクション(ロールバック)を実行して状態を元に戻します。

具体例:ECサイトでの注文処理

例えば、ECサイトでの注文処理を考えてみましょう:

  1. 在庫DBから商品の在庫を減らす
  2. 決済サービスで支払いを処理する
  3. 配送サービスに配送依頼を出す

これらの処理は別々のサービスで管理されており、それぞれが独自のデータベースを持っています。 従来の2フェーズコミットでは、すべてのサービスが同時にコミットできる状態になるまで待機する必要があり、パフォーマンスの低下やデッドロックのリスクがありました。

Sagaパターンでは、これらの処理を順番に実行し、途中で失敗した場合は、それまでに実行した処理を逆順でロールバックします:

  • 在庫DBの処理が失敗した場合:何もする必要がない(まだ在庫は減っていない)
  • 決済処理が失敗した場合:在庫を元に戻す
  • 配送依頼が失敗した場合:在庫を元に戻し、決済をキャンセルする

このように、各ステップで独立したトランザクションを実行し、失敗時には補償トランザクションを実行することで、分散システム全体の整合性を保ちながら、高いパフォーマンスを実現できます。

Sagaパターンの実装

マイクロサービスでSagaパターンを実装する場合、RabbitMQなどのメッセージキューを使って、各サービス間でのトランザクションの状態を管理することが一般的なようなのですが、 今回は1つのサーバーで複数のDBを使うマイクロサービスと比較するとシンプルな場合の、Sagaパターンを実装してみます。

SagaManager.ts

SagaManagerクラスは、トランザクションの各ステップを管理し、エラー発生時のロールバックを実行します。 各ステップはexecuterollbackの2つの関数を持ち、実行結果を保持して必要に応じてロールバックできるようにしています。

AuthService.ts

各ステップの説明

この例ではユーザーとクライアントのDBが別となっています。

  1. ユーザー情報の作成

    • ユーザーの基本情報を保存
    • 失敗時はユーザー情報を削除
  2. クライアント情報の作成

    • 企業情報を保存
    • 失敗時はクライアント情報を削除
  3. Stripeでの顧客作成

    • 決済システムに顧客情報を登録
    • 失敗時はStripeの顧客情報を削除
  4. メール認証トークンの作成

    • メール認証用のトークンを生成・保存
    • 失敗時はトークンを削除

参考

Thanks for the visit Nozo Blog