🔰

初心者のためのSagaパターン完全入門:分散トランザクションを基礎から理解する

に公開

はじめに

マイクロサービスを学習していると、必ず出会うのが**「分散トランザクション」という課題です。そして、その解決策としてSagaパターン**がよく紹介されます。

しかし、多くの記事では複雑なコード例から始まり、初心者には理解が困難です。本記事では、基礎の基礎からSagaパターンを学び、2つの実装方式の違いを明確に理解できるよう解説します。

まず、問題を理解しよう

従来のトランザクション(単一データベース)

まず、従来のデータベーストランザクションがどのように動作するかを確認しましょう。

例:銀行の送金システム(単一データベース)

-- 従来の方法:すべて同じデータベース内
BEGIN TRANSACTION;
  -- 田中さんの口座から1万円を引く
  UPDATE accounts SET balance = balance - 10000 WHERE name = '田中';
  
  -- 佐藤さんの口座に1万円を入れる  
  UPDATE accounts SET balance = balance + 10000 WHERE name = '佐藤';
COMMIT;

この場合、以下の特徴があります:

  • 全部成功 または 全部失敗 (ACID特性)
  • 途中で失敗したら、すべてが元に戻る(ロールバック)
  • データの整合性が常に保たれる

マイクロサービスでの問題

しかし、マイクロサービスでは状況が変わります:

🏦 Banking Service (銀行サービス)
├── accounts_db (口座データベース)

💰 Money Service (残高サービス)  
├── money_db (残高データベース)

📤 Remittance Service (送金サービス)
├── remittance_db (送金履歴データベース)

問題:複数のサービス・データベースにまたがる処理

1. Banking Service: 田中さんの銀行口座から1万円引く
2. Money Service: 田中さんのアプリ残高から1万円引く  
3. Money Service: 佐藤さんのアプリ残高に1万円入れる
4. Remittance Service: 送金履歴を記録する

何が問題なのか?

  • ステップ2で失敗したら、ステップ1をどう取り消す?
  • ステップ3で失敗したら、ステップ1,2をどう取り消す?
  • 各サービスは独立しているため、従来のトランザクションが使えない

Sagaパターンとは何か

基本的な考え方

Sagaパターンは、複数のサービスにまたがる処理を一連のローカルトランザクションとして実行し、失敗時には補償処理でロールバックする仕組みです。

重要な概念

  • ローカルトランザクション: 各サービス内での通常のトランザクション
  • 補償処理(Compensation): 失敗時に逆の操作を行う処理
  • 最終的一貫性: 即座にではなく、最終的にデータが整合する

具体例で理解しよう

送金処理をSagaで実装

📝 正常フロー:
Step 1: Banking Service → 田中さんの口座から1万円引く
Step 2: Money Service → 田中さんの残高から1万円引く
Step 3: Money Service → 佐藤さんの残高に1万円入れる  
Step 4: Remittance Service → 送金完了を記録

✅ 全て成功 → 送金完了!
⚠️ 失敗時の補償フロー:
Step 1: Banking Service → 田中さんの口座から1万円引く ✅
Step 2: Money Service → 田中さんの残高から1万円引く ✅  
Step 3: Money Service → 佐藤さんの残高に1万円入れる ❌ 失敗!

🔄 補償処理開始:
Compensation 2: Money Service → 田中さんの残高に1万円戻す
Compensation 1: Banking Service → 田中さんの口座に1万円戻す

❌ 送金失敗、すべて元通り

Sagaパターンの2つの実装方式

Sagaパターンには、誰がトランザクションを管理するかによって2つの方式があります。

1. Orchestration(オーケストレーション)- 中央管理方式

特徴:中央の指揮者(Orchestrator)が全体を管理

実装例(簡単なイメージ)

// 中央管理者(Orchestrator)
@Service
public class RemittanceOrchestrator {
    
    public void processRemittance(RemittanceRequest request) {
        try {
            // Step 1: 銀行口座から減額
            bankingService.deductFromAccount(request.getFromAccount(), request.getAmount());
            
            // Step 2: 送金者の残高から減額
            moneyService.deductBalance(request.getFromMember(), request.getAmount());
            
            // Step 3: 受取人の残高に加算
            moneyService.addBalance(request.getToMember(), request.getAmount());
            
            // Step 4: 送金完了記録
            remittanceService.recordSuccess(request);
            
        } catch (Exception e) {
            // 失敗時は補償処理を実行
            compensate(request, e);
        }
    }
    
    private void compensate(RemittanceRequest request, Exception failureReason) {
        // 逆順で補償処理を実行
        try {
            moneyService.compensateBalance(request.getFromMember(), request.getAmount());
            bankingService.compensateAccount(request.getFromAccount(), request.getAmount());
        } catch (Exception compensationError) {
            // 補償処理も失敗した場合のエラーハンドリング
            handleCompensationFailure(request, compensationError);
        }
    }
}

Orchestrationの特徴

メリット

  • 理解しやすい: 中央管理者が全体フローを制御
  • デバッグしやすい: 処理の流れが一箇所で確認できる
  • 複雑なビジネスロジック: 条件分岐や複雑なフローを実装しやすい

デメリット

  • 単一障害点: Orchestratorが止まると全体が止まる
  • 結合度が高い: Orchestratorが各サービスを知っている必要がある
  • スケーラビリティ: Orchestratorがボトルネックになる可能性

2. Choreography(コレオグラフィー)- 分散管理方式

特徴:各サービスが自分の役割を知っていて、イベントで連携

実装例(簡単なイメージ)

// Banking Service - 最初のステップ
@Service
public class BankingService {
    
    @EventListener
    public void handleRemittanceRequest(RemittanceRequestedEvent event) {
        try {
            // 口座から減額
            deductFromAccount(event.getFromAccount(), event.getAmount());
            
            // 成功イベントを発行
            eventPublisher.publish(new AccountDeductedEvent(
                event.getRemittanceId(),
                event.getFromMember(),
                event.getAmount()
            ));
        } catch (Exception e) {
            // 失敗イベントを発行
            eventPublisher.publish(new AccountDeductionFailedEvent(
                event.getRemittanceId(), e.getMessage()
            ));
        }
    }
}

// Money Service - 次のステップ
@Service  
public class MoneyService {
    
    @EventListener
    public void handleAccountDeducted(AccountDeductedEvent event) {
        try {
            // 送金者の残高から減額
            deductBalance(event.getFromMember(), event.getAmount());
            
            // 成功イベントを発行
            eventPublisher.publish(new SenderBalanceDeductedEvent(
                event.getRemittanceId(),
                event.getToMember(),
                event.getAmount()
            ));
        } catch (Exception e) {
            // 失敗時は補償イベントを発行
            eventPublisher.publish(new SenderBalanceDeductionFailedEvent(
                event.getRemittanceId(), e.getMessage()
            ));
        }
    }
    
    @EventListener
    public void handleSenderBalanceDeducted(SenderBalanceDeductedEvent event) {
        try {
            // 受取人の残高に加算
            addBalance(event.getToMember(), event.getAmount());
            
            // 成功イベントを発行
            eventPublisher.publish(new ReceiverBalanceAddedEvent(
                event.getRemittanceId()
            ));
        } catch (Exception e) {
            // 失敗時は補償イベントを発行(前のステップを取り消し)
            eventPublisher.publish(new ReceiverBalanceAdditionFailedEvent(
                event.getRemittanceId(), e.getMessage()
            ));
        }
    }
    
    // 補償処理
    @EventListener
    public void handleCompensation(SenderBalanceDeductionFailedEvent event) {
        // 送金者の残高を復元
        compensateBalance(event.getFromMember(), event.getAmount());
        
        // Banking Serviceに補償を依頼
        eventPublisher.publish(new CompensateAccountEvent(
            event.getRemittanceId(),
            event.getFromAccount(),
            event.getAmount()
        ));
    }
}

Choreographyの特徴

メリット

  • 疎結合: 各サービスが独立している
  • スケーラビリティ: 単一障害点がない
  • 拡張性: 新しいサービスを追加しやすい

デメリット

  • 複雑性: 全体の流れを把握するのが困難
  • デバッグの難しさ: 問題の原因を特定するのが大変
  • 循環依存: イベントの連鎖が複雑になりがち

どちらを選ぶべきか?

Orchestrationを選ぶべき場合

✅ こんな場合はOrchestration
- チームがマイクロサービス初心者
- ビジネスロジックが複雑
- デバッグのしやすさを重視
- 全体の流れを一箇所で管理したい
- トランザクションの種類が少ない

実例

  • 注文処理システム: 在庫確認 → 決済 → 配送手配 → 通知
  • 会員登録システム: 会員作成 → メール送信 → ポイント付与 → ウェルカムギフト

Choreographyを選ぶべき場合

✅ こんな場合はChoreography  
- 高いスケーラビリティが必要
- サービス間の独立性を重視
- イベント駆動アーキテクチャを採用
- 多くの異なるトランザクションパターンがある
- 各チームが独立して開発

実例

  • ECサイトの商品更新: 商品更新 → 検索インデックス更新 → キャッシュ更新 → 推薦システム更新
  • ユーザー行動分析: ユーザーアクション → ログ記録 → 分析処理 → レポート生成

実装時の重要なポイント

1. 冪等性の確保

https://zenn.dev/junsuk/articles/f678a6a762305c

問題: 同じ処理が複数回実行される可能性

// ❌ 悪い例:冪等性がない
@EventListener
public void handleMoneyDeduction(DeductMoneyEvent event) {
    // 毎回残高を減らしてしまう
    currentBalance = currentBalance - event.getAmount();
}

// ✅ 良い例:冪等性がある
@EventListener
public void handleMoneyDeduction(DeductMoneyEvent event) {
    // 既に処理済みかチェック
    if (isAlreadyProcessed(event.getTransactionId())) {
        return; // 重複処理を防ぐ
    }
    
    currentBalance = currentBalance - event.getAmount();
    markAsProcessed(event.getTransactionId());
}

2. タイムアウト処理

問題: 一部のサービスが応答しない場合

@Service
public class RemittanceOrchestrator {
    
    @Async
    public void processRemittance(RemittanceRequest request) {
        try {
            // タイムアウト付きで処理実行
            CompletableFuture<Void> future = CompletableFuture
                .runAsync(() -> executeRemittanceSteps(request))
                .orTimeout(30, TimeUnit.SECONDS); // 30秒でタイムアウト
                
            future.get();
            
        } catch (TimeoutException e) {
            // タイムアウト時の補償処理
            compensateRemittance(request, "Transaction timeout");
        }
    }
}

3. 状態の監視

重要: Sagaの進行状況を追跡できるようにする

// Sagaの状態を管理するエンティティ
@Entity
public class RemittanceSagaState {
    private String remittanceId;
    private SagaStatus status; // STARTED, STEP1_COMPLETED, STEP2_COMPLETED, FAILED, COMPLETED
    private String currentStep;
    private String errorMessage;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// 状態を更新するサービス
@Service
public class SagaStateService {
    
    public void updateSagaState(String remittanceId, SagaStatus status, String step) {
        RemittanceSagaState state = repository.findByRemittanceId(remittanceId);
        state.setStatus(status);
        state.setCurrentStep(step);
        state.setUpdatedAt(LocalDateTime.now());
        repository.save(state);
        
        // 監視システムに通知
        monitoringService.notifySagaStateChange(state);
    }
}

初心者向けの学習ステップ

Step 1: 単純なOrchestrationから始める

🌐 マイクロサービス構成
├── 📦 Order Service (Orchestrator) → localhost:8081
├── 📋 Inventory Service → localhost:8082
├── 💳 Payment Service → localhost:8083
└── 🚚 Shipping Service → localhost:8084

// 最もシンプルなSaga実装
@Service
public class SimpleOrderSaga {
    
    public void processOrder(OrderRequest request) {
        List<String> completedSteps = new ArrayList<>();
        
        try {
            // Step 1: 在庫確認
            inventoryService.reserveItem(request.getItemId(), request.getQuantity());
            completedSteps.add("INVENTORY_RESERVED");
            
            // Step 2: 決済処理
            paymentService.charge(request.getCustomerId(), request.getAmount());
            completedSteps.add("PAYMENT_CHARGED");
            
            // Step 3: 配送手配
            shippingService.arrangeShipping(request.getAddress());
            completedSteps.add("SHIPPING_ARRANGED");
            
            // 全て成功
            orderService.completeOrder(request.getOrderId());
            
        } catch (Exception e) {
            // 失敗時は逆順で補償
            compensateSteps(completedSteps, request);
        }
    }
    
    private void compensateSteps(List<String> completedSteps, OrderRequest request) {
        // 逆順で補償処理実行
        Collections.reverse(completedSteps);
        
        for (String step : completedSteps) {
            switch (step) {
                case "SHIPPING_ARRANGED":
                    shippingService.cancelShipping(request.getOrderId());
                    break;
                case "PAYMENT_CHARGED":
                    paymentService.refund(request.getCustomerId(), request.getAmount());
                    break;
                case "INVENTORY_RESERVED":
                    inventoryService.releaseItem(request.getItemId(), request.getQuantity());
                    break;
            }
        }
    }
}

Step 2: イベントベースのChoreographyを試す

// イベント定義
public class OrderCreatedEvent {
    private String orderId;
    private String customerId;
    private String itemId;
    private int quantity;
    private BigDecimal amount;
    // getters, setters...
}

// 各サービスでイベントを処理
@Component
public class InventoryEventHandler {
    
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            inventoryService.reserveItem(event.getItemId(), event.getQuantity());
            
            // 成功イベント発行
            eventPublisher.publishEvent(new InventoryReservedEvent(
                event.getOrderId(), 
                event.getItemId(), 
                event.getQuantity()
            ));
        } catch (Exception e) {
            // 失敗イベント発行
            eventPublisher.publishEvent(new InventoryReservationFailedEvent(
                event.getOrderId(), 
                e.getMessage()
            ));
        }
    }
}

Step 3: フレームワークを活用する

実際のプロジェクトでは、Axon FrameworkやEclipse MicroProfileなどのフレームワークを使用することをお勧めします。

まとめ

🎯 Sagaパターンの核心

  1. 分散トランザクション: 複数サービスにまたがる処理を管理
  2. 補償処理: 失敗時は逆の操作でロールバック
  3. 最終的一貫性: 即座にではなく、最終的にデータが整合

🔄 2つの実装方式

項目 Orchestration Choreography
管理方式 中央管理者が制御 各サービスが自律的に動作
理解しやすさ ⭐⭐⭐⭐⭐ ⭐⭐⭐
デバッグ ⭐⭐⭐⭐⭐ ⭐⭐
スケーラビリティ ⭐⭐⭐ ⭐⭐⭐⭐⭐
疎結合性 ⭐⭐ ⭐⭐⭐⭐⭐
初心者向け ⭐⭐⭐⭐⭐ ⭐⭐

🚀 学習の進め方

  1. まずはOrchestrationから: 理解しやすい中央管理方式で基本を学ぶ
  2. 小さな例から: 2-3ステップの簡単なSagaから始める
  3. 段階的に複雑化: 慣れてきたらChoreographyも試す
  4. 実際のフレームワーク: Axon FrameworkなどのツールでSagaを実装

Sagaパターンは最初は複雑に感じますが、**「失敗したら逆の操作をする」**という基本概念を理解すれば、マイクロサービスでの分散トランザクション管理が可能になります。

まずは簡単な例から始めて、徐々に複雑なパターンに挑戦してみてください!


参考資料

Discussion