マイクロサービスアーキテクチャを採用すると、システムは複数の自律的なサービスに分割され、それぞれが専用のデータストアを持つようになります。この環境では、複数のサービスにまたがるデータ操作の一貫性を保証する「分散トランザクション」の管理が極めて重要かつ困難な課題となります。EurekaはNetflix OSS由来のサービスディスカバリツールとして知られていますが、分散トランザクションの解決においては、トランザクションに関与するサービスの動的な特定と状態監視を行う基盤として機能します。本記事では、Eurekaを利用したサービス環境下において、Sagaパターンや2相コミット(2PC)を用いてデータ整合性を維持するための具体的なアプローチと実装例を解説します。
分散トランザクションが直面する主な課題
単一のデータベースで完結する伝統的なACIDトランザクションとは異なり、マイクロサービス間のトランザクションでは以下の問題に対処する必要があります。
- データの一貫性: 複数のサービスにまたがる操作が「すべて成功」するか「すべて失敗」するかを保証する必要があります。部分的な成功はデータの不整合を招きます。
- ネットワークの分断: サービス間の通信が不安定な場合、リクエストのタイムアウトや再試行によってトランザクションの状態が不明確になることがあります。
- サービスの障害: トランザクション処理中に特定のサービスがダウンした場合、システム全体の整合性を回復するためのメカニズム(補償処理など)が不可欠です。
Eurekaによるトランザクション管理のサポート
Eureka自体はトランザクション処理を行いませんが、分散トランザクションを実現するためのインフラストラクチャを提供します。
- 動的なサービス解決: トランザクションコーディネーターは、Eurekaを通じて在庫サービスや決済サービスなどの参加者インスタンスの実行場所(IPアドレスやポート)をリアルタイムに取得できます。
- ヘルスチェック: サービスインスタンスの死活監視を行い、応答しないインスタンスがトランザクションに参加しないようにすることができます。
実装パターン1:Sagaパターンによる長時間トランザクション
Sagaパターンは、大規模なトランザクションを複数のローカルトランザクションに分割し、各ステップで失敗した場合に逆方向の処理(補償トランザクション)を実行してデータを元の状態に戻す手法です。以下は、Spring Cloud環境での実装例です。
@Service
@RequiredArgsConstructor
public class OrderSagaOrchestrator {
private final DiscoveryClient discoveryClient;
private final RestTemplate restTemplate;
public void executeOrderFlow(OrderDto order) {
String inventoryServiceUrl = getServiceUrl("inventory-service");
String paymentServiceUrl = getServiceUrl("payment-service");
try {
// ステップ1: 在庫の確保
restTemplate.postForObject(inventoryServiceUrl + "/reserve", order, Void.class);
// ステップ2: 決済処理
restTemplate.postForObject(paymentServiceUrl + "/charge", order, Void.class);
} catch (RestClientException e) {
// 決済または在庫処理でエラーが発生した場合、補償処理を実行
compensateOrder(order, inventoryServiceUrl);
throw new TransactionFailedException("トランザクションが失敗し、ロールバックしました", e);
}
}
private void compensateOrder(OrderDto order, String inventoryServiceUrl) {
try {
// 在庫のロールバック(補償)
restTemplate.postForObject(inventoryServiceUrl + "/release", order, Void.class);
} catch (Exception ignore) {
// 補償処理の失敗はログに出力し、別途監視システムでアラート上げる等の対応が必要
}
}
private String getServiceUrl(String serviceName) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
if (instances.isEmpty()) {
throw new IllegalStateException("利用可能な " + serviceName + " が見つかりません");
}
// 簡易的なロードバランシング(実運用ではRibbonやSpring Cloud LoadBalancerを使用)
return instances.get(0).getUri().toString();
}
}
実装パターン2:2相コミット(2PC)のアプローチ
2相コミットは、すべての参加者がコミット可能かどうかを確認する「準備フェーズ」と、実際に処理を確定する「コミットフェーズ」の2段階で構成される厳密なプロトコルです。以下はそのロジックの概念実装です。
public class TwoPhaseCommitCoordinator {
public boolean executeTransaction(List<TransactionParticipant> participants) {
// フェーズ1: 準備
boolean allReady = true;
for (TransactionParticipant participant : participants) {
if (!participant.prepare()) {
allReady = false;
break;
}
}
// フェーズ2: コミットまたはロールバック
if (allReady) {
participants.forEach(TransactionParticipant::commit);
return true;
} else {
// 1つでも準備できなければ全員ロールバック
participants.forEach(TransactionParticipant::rollback);
return false;
}
}
}
interface TransactionParticipant {
boolean prepare(); // リソースのロックと処理準備
void commit(); // 変更の確定
void rollback(); // 変更の破棄
}
サービス間の調整と状態管理
分散トランザクションにおいては、処理の途中経過をどこかに記録しておくことが重要です。Eurekaは本来メタデータストアではありませんが、サービスのメタデータ機能を活用して、簡易的な状態情報やトランザクションコンテキストのヒントを保持させることも可能です。しかし、大規模なシステムでは、トランザクションコーディネーターが専用のデータストアを持つか、イベントソーシングを採用する方が一般的です。以下は、Eurekaを通じて特定の役割を持つサービスを検索する例です。
@Component
public class ServiceRegistryClient {
@Autowired
private EurekaClient eurekaClient;
public InstanceInfo getCoordinatorInstance() {
Application app = eurekaClient.getApplication("tx-coordinator-service");
if (app == null) {
return null;
}
// 特定のメタデータを持つインスタンスを検索
return app.getInstances().stream()
.filter(instance -> "active".equals(instance.getMetadata().get("status")))
.findFirst()
.orElse(null);
}
}
監視と補償処理の自動化
分散環境では、タイムアウト等によりトランザクションが中途半端な状態( dangling state )になるリスクがあります。これに対処するため、定期的な監視プロセスが不完全なトランザクションを検出し、適切な補償アクションをトリガーする仕組みが必要です。
@Service
public class TransactionMonitorJob {
@Autowired
private TransactionLogRepository logRepository;
@Scheduled(fixedRate = 60000) // 1分ごとに実行
public void checkPendingTransactions() {
List<TransactionLog> pendingLogs = logRepository.findByStatus(TransactionStatus.PENDING);
for (TransactionLog log : pendingLogs) {
if (log.isExpired()) {
// タイムアウトしている場合、補償処理を実行
compensateTransaction(log);
}
}
}
private void compensateTransaction(TransactionLog log) {
// ビジネスロジックに基づいて、各サービスに対してキャンセル処理を呼び出す
// ...
log.setStatus(TransactionStatus.COMPENSATED);
logRepository.save(log);
}
}
マイクロサービスにおける分散トランザクションは、複雑さを伴いますが、Eurekaによる動的なサービス検出と、Sagaや2PCなどの適切なパターン選択、そして徹底した状態監視を組み合わせることで、堅牢なシステムを構築することが可能です。実装の際には、CAP定理のトレードオフを考慮し、ビジネス要件に最適な整合性レベルを選択することが求められます。