Chuyển đến nội dung chính

レッスン 10: Saga パターン — 分散トランザクション

2PC がマイクロサービス、Saga パターン (コレオグラフィーとオーケストレーション)、補償トランザクション、Saga Orchestrator の実装、エラー処理、デッド レター キューに適さない理由。

🏗️ アーキテクチャ — レッスン 10 レッスン 10: サーガ パターン — 分散 取引

クラウドネイティブのマイクロサービスアーキテクチャ

パート 3: マイクロサービスにおけるデータ管理

xdev.asia

レッスン 10: Saga パターン — 分散トランザクション

はじめに

モノリスでは、ビジネス トランザクションに同じ ACID トランザクションに複数のデータベース操作を含めることができます。マイクロサービスでは、各サービスが独自のデータベースを持ちます。従来の分散トランザクション (2PC) は使用できません。これは、密結合が生じてパフォーマンスに影響を与えるためです。 Saga パターンは代替案です。


1. 分散トランザクションの問題

1.1 2 フェーズ コミット (2PC) — 不適切な理由

2PC Flow:
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│ Transaction  │    │  Service A   │    │  Service B   │
│ Coordinator  │    │  (Order)     │    │  (Payment)   │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │
       │── Phase 1: PREPARE ──────────────────▶│
       │◀── VOTE YES ────────────────────────── │
       │── Phase 1: PREPARE ──▶│               │
       │◀── VOTE YES ──────── │               │
       │                      │               │
       │── Phase 2: COMMIT ──▶│               │
       │── Phase 2: COMMIT ──────────────────▶│
       │                      │               │

マイクロサービスにおける 2PC の問題:

問題説明
単一障害点コーディネーターが死亡 → すべての参加者がロック
パフォーマンスのボトルネック2PC 中にリソースをロック
密結合すべてのサービスを同時に利用できる必要があります
スケーラブルではないロック競合は参加者の数に応じて増加します。
ネットワークパーティションフェーズ 1 とフェーズ 2 の間でネットワークが切断された場合 → 不整合状態

1.2 CAP 定理の繰り返し

分散システムでは、3 つのうち 2 つだけを達成できます。

  • 一貫性: すべての読み取りが最新の書き込みを返します。
  • 可用性: すべてのリクエストは応答を受け取ります
  • 分割耐性: ネットワークが分割されていてもシステムは動作し続けます。

マイクロサービスは AP (可用性 + パーティション許容値) を選択し、結果整合性 を受け入れます。


2. サーガパターン

2.1 概念

Saga は一連の ローカル トランザクション であり、各トランザクションはサービスによって実行されます。ステップが失敗した場合、サガは補償トランザクションを実行して前のステップを元に戻します。

Saga = T1 → T2 → T3 → ... → Tn

Nếu Ti fail:
  Compensate: C(i-1) → C(i-2) → ... → C1

Ví dụ Order Saga:
  T1: Create Order (status: PENDING)
  T2: Reserve Payment
  T3: Reserve Inventory
  T4: Confirm Order (status: CONFIRMED)

Nếu T3 fail:
  C2: Refund Payment
  C1: Cancel Order (status: CANCELLED)

2.2 補償取引

物語内の各トランザクションには、対応する補償トランザクションが必要です。

ステップアクション補償アクション
T1注文の作成注文をキャンセル
T2予約支払い払い戻し
T3在庫を予約するリリースインベントリ
T4発送スケジュール配送をキャンセル
T5確認メールを送信キャンセルメールを送信

重要な注意事項:

  • 補償トランザクションは 冪等 である必要があります (複数回実行しても同じ結果になります)
  • 補償トランザクション 失敗できません (成功するまで再試行する必要があります)
  • 一部のアクションは補償できません(送信済みメールの送信など) → 「セマンティックアンドゥ」を使用(キャンセルされたメールを送信)

3. 振付サーガ

3.1 概念

各サービスはローカル トランザクションの完了時に イベントをパブリッシュし、次のサービスは サブスクライブして次の処理を行います。

┌──────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  Order   │    │   Payment    │    │  Inventory   │    │  Shipping    │
│  Service │    │   Service    │    │  Service     │    │  Service     │
└────┬─────┘    └──────┬───────┘    └──────┬───────┘    └──────┬───────┘
     │                 │                   │                   │
     │── OrderCreated ▶│                   │                   │
     │                 │── PaymentReserved ▶│                   │
     │                 │                   │── InventoryReserved ▶│
     │                 │                   │                   │── ShipmentScheduled
     │◀───────────────────────────────────────────────────────── │
     │  OrderConfirmed │                   │                   │
     │                 │                   │                   │
     │ === FAILURE === │                   │                   │
     │                 │                   │── InventoryFailed ▶│
     │                 │◀─ CompensatePayment─                  │
     │◀─ PaymentRefunded─                  │                   │
     │  OrderCancelled │                   │                   │

3.2 実装例

// Order Service — publishes OrderCreated
@Service
public class OrderService {

    @Transactional
    public Order createOrder(CreateOrderCommand cmd) {
        Order order = new Order(cmd.getCustomerId(), cmd.getItems());
        order.setStatus(OrderStatus.PENDING);
        orderRepository.save(order);

        // Publish event
        eventPublisher.publish(new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getItems(),
            order.getTotalAmount()
        ));

        return order;
    }

    // Compensating handler
    @EventHandler
    public void on(PaymentFailedEvent event) {
        Order order = orderRepository.findById(event.getOrderId());
        order.setStatus(OrderStatus.CANCELLED);
        order.setFailureReason(event.getReason());
        orderRepository.save(order);
    }
}

// Payment Service — listens to OrderCreated
@Service
public class PaymentService {

    @EventHandler
    public void on(OrderCreatedEvent event) {
        try {
            Payment payment = paymentGateway.reserve(
                event.getCustomerId(),
                event.getTotalAmount()
            );
            eventPublisher.publish(new PaymentReservedEvent(
                event.getOrderId(),
                payment.getId()
            ));
        } catch (InsufficientFundsException e) {
            eventPublisher.publish(new PaymentFailedEvent(
                event.getOrderId(),
                "Insufficient funds"
            ));
        }
    }
}

3.3 メリットとデメリット

利点デメリット
サービス間の疎結合複雑なフローを追跡するのが難しい
シンプルで少ない手順の物語循環依存関係が発生する可能性があります。
単一障害点はありませんエラーがあるとデバッグが困難
イベント駆動型アーキテクチャによる自然な複雑なテスト

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

4.1 概念

中央の Saga Orchestrator がワークフロー全体を調整し、各サービスにコマンドを送信して応答を処理します。

                    ┌──────────────────────┐
                    │  Order Saga          │
                    │  Orchestrator        │
                    │                      │
                    │  State Machine:      │
                    │  CREATED             │
                    │  → PAYMENT_PENDING   │
                    │  → INVENTORY_PENDING │
                    │  → SHIPPING_PENDING  │
                    │  → CONFIRMED         │
                    │  or → COMPENSATING   │
                    │  → CANCELLED         │
                    └────────┬─────────────┘
                             │
            ┌────────────────┼────────────────┐
            │                │                │
     ┌──────▼──────┐  ┌─────▼──────┐  ┌──────▼──────┐
     │  Payment    │  │ Inventory  │  │  Shipping   │
     │  Service    │  │ Service    │  │  Service    │
     └─────────────┘  └────────────┘  └─────────────┘

4.2 ステートマシンの実装

public class OrderSaga {

    public enum State {
        CREATED,
        PAYMENT_PENDING,
        PAYMENT_RESERVED,
        INVENTORY_PENDING,
        INVENTORY_RESERVED,
        SHIPPING_PENDING,
        CONFIRMED,
        COMPENSATING_INVENTORY,
        COMPENSATING_PAYMENT,
        CANCELLED
    }

    @Autowired
    private SagaRepository sagaRepository;

    public void start(CreateOrderCommand cmd) {
        SagaState saga = new SagaState(cmd.getOrderId(), State.CREATED);
        sagaRepository.save(saga);
        
        // Step 1: Reserve Payment
        saga.setState(State.PAYMENT_PENDING);
        commandGateway.send(new ReservePaymentCommand(
            cmd.getOrderId(), cmd.getAmount()
        ));
    }

    @SagaEventHandler
    public void on(PaymentReservedEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.INVENTORY_PENDING);
        
        // Step 2: Reserve Inventory
        commandGateway.send(new ReserveInventoryCommand(
            event.getOrderId(), saga.getItems()
        ));
    }

    @SagaEventHandler
    public void on(InventoryReservedEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.SHIPPING_PENDING);
        
        // Step 3: Schedule Shipping
        commandGateway.send(new ScheduleShippingCommand(
            event.getOrderId(), saga.getAddress()
        ));
    }

    @SagaEventHandler
    public void on(ShipmentScheduledEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.CONFIRMED);
        
        commandGateway.send(new ConfirmOrderCommand(event.getOrderId()));
    }

    // === COMPENSATION ===
    
    @SagaEventHandler
    public void on(InventoryReservationFailedEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.COMPENSATING_PAYMENT);
        
        // Compensate: Refund Payment
        commandGateway.send(new RefundPaymentCommand(
            event.getOrderId(), saga.getPaymentId()
        ));
    }

    @SagaEventHandler
    public void on(PaymentRefundedEvent event) {
        SagaState saga = sagaRepository.findByOrderId(event.getOrderId());
        saga.setState(State.CANCELLED);
        
        commandGateway.send(new CancelOrderCommand(
            event.getOrderId(), "Inventory not available"
        ));
    }
}

4.3 メリットとデメリット

利点デメリット
わかりやすいワークフロー(一元化)オーケストレーターは単一障害点です
デバッグと監視が簡単神クラスのリスク(論理的すぎる)
ステップの追加/削除が簡単オーケストレーターとサービス間の結合
補償ロジックを明確に永続的な saga 状態が必要

5. コレオグラフィーとオーケストレーションを比較する

基準振付オーケストレーション
カップリング非常に緩い中 (オーケストレーター経由)
複雑さサービス数に応じて増加オーケストレーターに焦点を当てる
可視性全体の流れが見えにくい|ステートマシンで明示的に
障害対応分散フォーカス
テスト難しい(分散)より簡単 (テスト オーケストレーター)
スケーラビリティ良い良い (オーケストレーター無国籍)
推奨≤ 佐賀のサービスは 4 つまで> 4 つのサービスまたは複雑なフロー

6. エラー処理とデッドレターキュー

6.1 デッドレターキュー (DLQ)

複数回再試行しても処理できないメッセージは DLQ に渡されます。

                    ┌─────────────┐
                    │  Main Queue │
                    │ (order.cmds)│
                    └──────┬──────┘
                           │
                    ┌──────▼──────┐
                    │  Consumer   │
                    │  (Service)  │
                    └──────┬──────┘
                           │
                  Success? ─┼─ No (after max retries)
                  │         │
                  ▼         ▼
               ┌─────┐  ┌──────────┐
               │ ACK │  │   DLQ    │
               └─────┘  │(order.   │
                         │ cmds.dlq)│
                         └──────────┘
                              │
                         ┌────▼────┐
                         │ Alert + │
                         │ Manual  │
                         │ Review  │
                         └─────────┘

6.2 べき等性

同じ結果を得るために、必ずメッセージを複数回処理してください。

@Service
public class PaymentService {

    @EventHandler
    public void on(ReservePaymentCommand cmd) {
        // Idempotency check
        String idempotencyKey = "payment:" + cmd.getOrderId();
        if (processedStore.exists(idempotencyKey)) {
            log.info("Already processed payment for order {}", cmd.getOrderId());
            return; // Skip duplicate
        }

        Payment payment = processPayment(cmd);

        // Mark as processed
        processedStore.save(idempotencyKey, payment.getId());
    }
}

6.3 サーガタイムアウト

public class OrderSaga {

    @SagaTimeout(duration = "5m")
    public void onTimeout(SagaState saga) {
        log.warn("Saga timeout for order {}", saga.getOrderId());
        
        // Compensate based on current state
        switch (saga.getState()) {
            case INVENTORY_PENDING:
                compensatePayment(saga);
                break;
            case SHIPPING_PENDING:
                compensateInventory(saga);
                compensatePayment(saga);
                break;
        }
        
        saga.setState(State.CANCELLED);
        saga.setFailureReason("Saga timeout");
    }
}

7. ベストプラクティス

7.1 Saga の設計ガイドライン

1. Mỗi step phải có compensating action
2. Compensating actions phải idempotent
3. Sử dụng correlation ID (saga ID) xuyên suốt
4. Persist saga state (survive service restart)
5. Set timeout cho mỗi saga instance
6. Monitor saga metrics (success rate, duration, failure reasons)
7. DLQ cho messages không xử lý được
8. Tránh saga quá nhiều steps (> 7 steps → xem lại design)

7.2 コリオグラフィーかオーケストレーションを選択しますか?

Flow đơn giản (2-4 services)?
  └── Choreography

Flow phức tạp (> 4 services, conditional logic)?
  └── Orchestration

Cần visibility cao vào business process?
  └── Orchestration

Muốn minimize coupling?
  └── Choreography

Team experience với event-driven?
  ├── Nhiều → Choreography
  └── Ít → Orchestration

概要

  • 2PC は密結合とロック競合のため、マイクロサービスには適していません
  • Saga パターン は、ローカル トランザクション + 補償トランザクションのチェーンを使用します
  • コレオグラフィー: サービスはイベントを介して分散的に通信します。
  • オーケストレーション: Saga Orchestrator はステート マシンを通じて一元的に調整します。
  • 補償トランザクションは 冪等 である必要があり、失敗してはなりません
  • エラー処理には DLQ、タイムアウト、冪等性を使用します