7つのリトライパターンをすべてのバックエンドエンジニアが知っておくべき
Wenhao Wang
Dev Intern · Leapcell

序文
ビジネスシステムでは、障害は日常茶飯事です。ネットワークの変動、サービスの過負荷、不安定なサードパーティインターフェースなど、システムは時折発生する異常に対処するために「自己修復」機能を備えている必要があります。
リトライメカニズムは、システムの自己回復能力の主要なコンポーネントの1つです。
ただし、リトライは両刃の剣です。適切に設計すれば、成功率が向上し、ユーザーエクスペリエンスが向上します。設計が不十分な場合、リクエストの嵐、連鎖的な障害につながり、問題をインシデントにまで拡大する可能性があります。
この記事では、一般的に使用される7つのリトライ戦略について説明します。
1. 総当たりループ
問題のシナリオ
ユーザー登録SMS送信インターフェースが、whileループでサードパーティのSMS APIを繰り返し呼び出します。
コード例:
public void sendSms(String phone) { int retry = 0; while (retry < 5) { // 総当たりループ try { smsClient.send(phone); break; } catch (Exception e) { retry++; Thread.sleep(1000); // 固定1秒遅延 } } }
インシデント
SMSサーバーが過負荷になり、すべてのリクエストが3秒遅延しました。
この総当たりコードは、0.5秒以内に数万回のリトライをトリガーし、SMSプラットフォームを圧倒し、サーキットブレーカーをトリガーし、通常のリクエストさえ拒否しました。
教訓:
- 遅延間隔の調整なし:固定遅延によりリトライが集中
- 例外タイプの無視:一時的でないエラー(無効なパラメータなど)でもリトライ
- 修正:ランダムな遅延を導入し、リトライできない例外を除外
2. Spring Retry
ユースケース
Spring Retryは、小規模から中規模のプロジェクトに適しており、アノテーションを通じて基本的なリトライとサーキットブレーカー(オーダーステータス照会APIなど)を迅速に実装できます。
@Retryable
アノテーションを使用すると、リトライロジックが実装されます。
設定例
@Retryable( value = {TimeoutException.class}, // タイムアウトの場合のみリトライ maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2) // 1秒 → 2秒 → 4秒 ) public boolean queryOrderStatus(String orderId) { return httpClient.get("/order/" + orderId); } @Recover // フォールバックメソッド public boolean fallback() { return false; }
利点
- 宣言的なアノテーション:クリーンなコード、ビジネスロジックから分離
- 指数関数的なバックオフ:リトライ間隔を自動的に増加
- サーキットブレーカーの統合:
@CircuitBreaker
と組み合わせて、障害トラフィックを迅速にブロック
3. Resilience4j
高度なシナリオ
カスタムバックオフアルゴリズム、サーキットブレーカー戦略、および多層保護(コア決済APIなど)を必要とする、より複雑なシステムには、Resilience4jが推奨されます。
コアコード:
// 1. リトライ設定:指数関数的なバックオフ+ジッター RetryConfig retryConfig = RetryConfig.custom() .maxAttempts(3) .intervalFunction(IntervalFunction.ofExponentialRandomBackoff( 1000L, // 初期1秒遅延 2.0, // 指数関数的な乗数 0.3 // ジッター係数 )) .retryOnException(e -> e instanceof TimeoutException) .build(); // 2. サーキットブレーカー設定:障害率が50%を超える場合にトリガー CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom() .slidingWindow(10, 10, CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) .failureRateThreshold(50) .build(); // 組み合わせた使用 Retry retry = Retry.of("payment", retryConfig); CircuitBreaker cb = CircuitBreaker.of("payment", cbConfig); // ビジネスロジックの実行 Supplier<Boolean> supplier = () -> paymentService.pay(); Supplier<Boolean> decorated = Decorators.ofSupplier(supplier) .withRetry(retry) .withCircuitBreaker(cb) .decorate();
結果
このソリューションをデプロイした後、ある企業の決済APIのタイムアウト率は60%低下し、サーキットブレーカーのトリガー頻度はほぼ90%低下しました。
4. MQキュー
ユースケース
高並行性、遅延耐性のある非同期シナリオ(ロジスティクスステータスの同期など)。
実装原則
- 最初のリクエストが失敗した場合、メッセージは遅延キューに送信されます。
- キューは、事前設定された遅延(5秒、30秒、1分など)の後、メッセージの消費をリトライします。
- 最大リトライ回数に達した場合、メッセージは手動処理のためにデッドレターキューに移動されます。
RocketMQコードスニペット:
// プロデューサーは遅延メッセージを送信します Message<String> message = new Message(); message.setBody("注文データ"); message.setDelayTimeLevel(3); // RocketMQレベル3 = 10秒遅延 rocketMQTemplate.send(message); // コンシューマーはリトライします @RocketMQMessageListener(topic = "DELAY_TOPIC") public class DelayConsumer { @Override public void handleMessage(Message message) { try { syncLogistics(message); } catch (Exception e) { // 遅延レベルを上げてリトライします resendWithDelay(message, retryCount + 1); } } }
RocketMQは、失敗したコンシューマーの操作を自動的にリトライします。
5. スケジュールされたタスク
ユースケース
リアルタイムの応答を必要とせず、バッチ処理を許可するタスク(ファイルのインポートなど)には、スケジュールされたジョブを使用できます。
Quartzを使用した例:
@Scheduled(cron = "0 0/5 * * * ?") // 5分ごとに実行 public void retryFailedTasks() { List<FailedTask> list = failedTaskDao.listUnprocessed(5); // 失敗したタスクを照会 list.forEach(task -> { try { retryTask(task); task.markSuccess(); } catch (Exception e) { task.incrRetryCount(); } failedTaskDao.update(task); }); }
6. Two-Phase Commit
ユースケース
厳密なデータ整合性を必要とするシナリオ(資金移動など)には、Two-Phase Commitメカニズムを使用できます。
主要な実装
- **フェーズ1:**トランザクションをデータベースに記録します(ステータスは「保留中」に設定)。
- **フェーズ2:**リモートインターフェースを呼び出し、結果に基づいてトランザクションステータスを更新します。
- **補正タスク:**タイムアウトした「保留中」のトランザクションを定期的にスキャンしてリトライします。
サンプルコード:
@Transactional public void transfer(TransferRequest req) { // 1. トランザクションを記録します transferRecordDao.create(req, PENDING); // 2. 銀行APIを呼び出します boolean success = bankClient.transfer(req); // 3. トランザクションステータスを更新します transferRecordDao.updateStatus(req.getId(), success ? SUCCESS : FAILED); // 4. 失敗した場合は非同期的にリトライします if (!success) { mqTemplate.send("TRANSFER_RETRY_QUEUE", req); } }
7. 分散ロック
ユースケース
べき等性が重要な複数のサービスインスタンスまたはマルチスレッド環境(フラッシュセールなど)では、分散ロックを使用できます。
Redis + Luaを使用した分散ロックの例:
public boolean retryWithLock(String key, int maxRetry) { String lockKey = "api_retry_lock:" + key; for (int i = 0; i < maxRetry; i++) { // 分散ロックの取得を試みます if (redis.setnx(lockKey, "1", 30, TimeUnit.SECONDS)) { try { return callApi(); } finally { redis.delete(lockKey); } } Thread.sleep(1000 * (i + 1)); // リトライ前に待機 } return false; }
まとめ
リトライメカニズムは、データセンターの消火器のようなものです。使用する必要がないことを願っていますが、災害が発生したときには、最後の防衛線になる可能性があります。
職場でどのソリューションを選択すべきでしょうか?
最新の技術トレンドをただ追いかけるのではなく、ビジネスに必要な攻守のバランスに基づいて選択してください。
システムの安定性の鍵は、常にリトライを尊重することにあります。
Leapcellは、バックエンドプロジェクトをホストするための最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い、リクエストも料金も発生しません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:$ 25は、平均応答時間60ミリ秒で694万件のリクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI / CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムのメトリックとロギング。
簡単なスケーラビリティと高性能
- 高い並行処理を簡単に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロで、構築に集中できます。
ドキュメントで詳細をご覧ください。
Xでフォローしてください:@LeapcellHQ