0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

システム設計に役立つ7つのリトライ手法

Posted at

表紙

はじめに

業務システムにおいて、障害は日常茶飯事です。ネットワークの不安定、サービスの過負荷、あるいは外部 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 キュー

適用シーン

高トラフィックで多少の遅延が許容される非同期シナリオ(例:物流ステータスの同期)に適しています。

実現原理

  1. 初回リクエストが失敗したら、メッセージを遅延キューに投入
  2. キューは事前に設定された遅延時間(例:5 秒、30 秒、1 分)に基づいて再試行を行う
  3. 最大リトライ回数に達した場合、デッドレターキュー(人工対応)に移送される

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. フェーズ 1:操作ログを DB に記録(ステータスは「処理中」)
  2. フェーズ 2:リモート API を呼び出し、結果に応じてログの状態を更新
  3. タイマー補償:タイムアウトした「処理中」ログをスキャンし、再試行

コード例:

@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

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?