初心者のための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. 冪等性の確保
問題: 同じ処理が複数回実行される可能性
// ❌ 悪い例:冪等性がない
@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パターンの核心
- 分散トランザクション: 複数サービスにまたがる処理を管理
- 補償処理: 失敗時は逆の操作でロールバック
- 最終的一貫性: 即座にではなく、最終的にデータが整合
🔄 2つの実装方式
| 項目 | Orchestration | Choreography |
|---|---|---|
| 管理方式 | 中央管理者が制御 | 各サービスが自律的に動作 |
| 理解しやすさ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| デバッグ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| スケーラビリティ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 疎結合性 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 初心者向け | ⭐⭐⭐⭐⭐ | ⭐⭐ |
🚀 学習の進め方
- まずはOrchestrationから: 理解しやすい中央管理方式で基本を学ぶ
- 小さな例から: 2-3ステップの簡単なSagaから始める
- 段階的に複雑化: 慣れてきたらChoreographyも試す
- 実際のフレームワーク: Axon FrameworkなどのツールでSagaを実装
Sagaパターンは最初は複雑に感じますが、**「失敗したら逆の操作をする」**という基本概念を理解すれば、マイクロサービスでの分散トランザクション管理が可能になります。
まずは簡単な例から始めて、徐々に複雑なパターンに挑戦してみてください!
参考資料
Discussion