はじめに
業務システムにおいて、障害は日常茶飯事です。ネットワークの不安定、サービスの過負荷、あるいは外部 API の不安定さなど、システムは突発的な異常に対処する「自己回復力」を備えていなければなりません。
リトライメカニズムは、その自己回復力の中核の一つです。
しかし、リトライは諸刃の剣でもあります。うまく設計すれば成功率を高め、ユーザー体験を改善できますが、設計を誤ればリクエストの嵐や雪崩のような障害を引き起こし、問題を事故にまで拡大させかねません。
この記事では、よく使われるリトライの 7 つの手法について紹介します。
1. 暴力ループ法(無条件ループ)
問題の場面
ユーザー登録時に SMS を送信する API を、while
ループで繰り返し第三者の SMS 送信インターフェースを呼び出すケース。
コード例:
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
適用シーン
中小規模のプロジェクトに適しており、アノテーションによって基本的なリトライとサーキットブレーカーが実現可能です(例:注文状況の確認 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)に適しています。
主なコードは以下の通り:
// 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 事前定義の10秒遅延レベル
rocketMQTemplate.send(message);
// コンシューマー側の再試行処理
@RocketMQMessageListener(topic = "DELAY_TOPIC")
public class DelayConsumer {
@Override
public void handleMessage(Message message) {
try {
syncLogistics(message);
} catch (Exception e) {
// リトライ回数 +1 して、より長い遅延で再送
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. 2 フェーズコミット(Two-Phase Commit)
適用シーン
データの整合性を厳密に保証する必要がある場面(例:資金移動)において使用されます。
実装のポイント
- フェーズ 1:操作ログを DB に記録(ステータスは「処理中」)
- フェーズ 2:リモート API を呼び出し、結果に応じてログの状態を更新
- タイマー補償:タイムアウトした「処理中」ログをスキャンし、再試行
コード例:
@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で6.94Mリクエスト、平均応答時間60ms。
洗練された開発者体験
- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
Xでフォローする:@LeapcellHQ