問題: ホテル予約における分散トランザクション

ホテル予約システムを基本的なコンポーネントに分解してみましょう:

  • 予約サービス: 部屋の空き状況と予約を管理
  • 支払いサービス: 支払いを処理
  • 通知サービス: 確認メールを送信
  • ロイヤルティサービス: 顧客のポイントを更新

さて、顧客が部屋を予約するシナリオを想像してみてください。私たちは以下を行う必要があります:

  1. 部屋の空き状況を確認し、予約する
  2. 支払いを処理する
  3. 確認メールを送信する
  4. 顧客のロイヤルティポイントを更新する

簡単に聞こえますよね?でも、そう簡単ではありません。支払いが失敗した場合、または通知サービスがダウンしている場合はどうなるでしょうか?これが分散トランザクションの世界で、マーフィーの法則が常に働いています。

サガの登場: 分散トランザクションの無名のヒーロー

サガは、各トランザクションが単一のサービス内でデータを更新する一連のローカルトランザクションです。ステップが失敗した場合、サガは前のステップで行われた変更を元に戻す補償トランザクションを実行します。

ホテル予約のサガは次のようになります:


def book_hotel_room(customer_id, room_id, payment_info):
    try:
        # ステップ1: 部屋を予約する
        reservation_id = reservation_service.reserve_room(room_id)
        
        # ステップ2: 支払いを処理する
        payment_id = payment_service.process_payment(payment_info)
        
        # ステップ3: 確認を送信する
        notification_service.send_confirmation(customer_id, reservation_id)
        
        # ステップ4: ロイヤルティポイントを更新する
        loyalty_service.update_points(customer_id, calculate_points(room_id))
        
        return "予約が成功しました!"
    except Exception as e:
        # どのステップでも失敗した場合、補償アクションを実行する
        compensate_booking(reservation_id, payment_id, customer_id)
        raise e

def compensate_booking(reservation_id, payment_id, customer_id):
    if reservation_id:
        reservation_service.cancel_reservation(reservation_id)
    if payment_id:
        payment_service.refund_payment(payment_id)
    notification_service.send_cancellation(customer_id)
    # ロイヤルティポイントはまだ追加されていないので補償は不要

冪等性の実装: 一度では十分でないこともある

分散システムでは、ネットワークの問題で重複したリクエストが発生することがあります。これに対処するために、操作を冪等にする必要があります。冪等性キーを使います:


def reserve_room(room_id, idempotency_key):
    if reservation_exists(idempotency_key):
        return get_existing_reservation(idempotency_key)
    
    # 実際の予約ロジックを実行
    reservation = create_reservation(room_id)
    store_reservation(idempotency_key, reservation)
    return reservation

クライアントによって生成されたUUIDなどの冪等性キーを使用することで、同じリクエストが複数回送信されても、予約は一度だけ作成されます。

非同期ロールバック: 時間はトランザクションを待たない

時には、補償アクションをすぐに実行できないことがあります。例えば、支払いサービスが一時的にダウンしている場合、すぐに返金を行うことはできません。ここで非同期ロールバックが役立ちます:


def compensate_booking_async(reservation_id, payment_id, customer_id):
    compensation_tasks = [
        {'service': 'reservation', 'action': 'cancel', 'id': reservation_id},
        {'service': 'payment', 'action': 'refund', 'id': payment_id},
        {'service': 'notification', 'action': 'send_cancellation', 'id': customer_id}
    ]
    
    for task in compensation_tasks:
        compensation_queue.enqueue(task)

# 別のワーカープロセスで
def process_compensation_queue():
    while True:
        task = compensation_queue.dequeue()
        try:
            execute_compensation(task)
        except Exception:
            # 補償が失敗した場合、指数バックオフで再キュー
            compensation_queue.requeue(task, delay=calculate_backoff(task))

このアプローチにより、サービスが一時的に利用できない場合でも、補償を確実に処理できます。

落とし穴: 何が問題になる可能性があるか?

サガは強力ですが、課題もあります:

  • 複雑さ: 各ステップの補償アクションを実装するのは難しいことがあります。
  • 最終的な一貫性: システムが一時的に不整合な状態になることがあります。
  • 隔離の欠如: 他のトランザクションが中間状態を見る可能性があります。

これらの問題を軽減するために:

  • サガオーケストレーターを使用してワークフローと補償を管理します。
  • 堅牢なエラーハンドリングとログを実装します。
  • 重要なリソースには悲観的ロックを検討します。

報酬: なぜこれを行うのか?

「これは多くの作業のように見えます。なぜ2PCを使わないのか?」と思うかもしれません。理由は以下の通りです:

  • スケーラビリティ: サガは長期間のロックを必要としないため、より良いスケーラビリティを提供します。
  • 柔軟性: サービスは独立して更新でき、トランザクション全体を壊すことはありません。
  • 回復力: 一部のサービスが一時的にダウンしても、システムは機能し続けることができます。
  • パフォーマンス: 分散ロックが不要なため、全体的なトランザクション処理が速くなります。

まとめ: 重要なポイント

補償ワークフローとサガを使用して2PCなしで分散トランザクションを実装することは、ホテル予約プラットフォームのような複雑なシステムに対する堅牢でスケーラブルなソリューションを提供します。冪等性キーと非同期ロールバックを活用することで、障害を優雅に処理し、マイクロサービス間でデータの一貫性を確保する堅牢なシステムを構築できます。

目標は失敗を避けることではなく(分散システムでは避けられません)、それを優雅に処理することです。サガを使用することで、単にホテルの部屋を予約するだけでなく、より信頼性が高くスケーラブルな分散トランザクションの世界に足を踏み入れることができます。

"分散システムでは、失敗は可能性ではなく、必然です。失敗を想定して設計すれば、成功に向けて構築できます。"

さあ、進んで、あなたのトランザクションが常にうまくいくことを願っています!

さらなる学び

コーディングを楽しんで、あなたの分散トランザクションが常にスムーズで補償されることを願っています!