遠端呼叫爆了,重試一下通常就活了。但如果一直狂轟,server 只會更爆。所以咱們要聰明一點,用指數退避(exponential backoff)的方式慢慢來。

為啥要指數退避

直接 retry 沒有 delay 的問題:

  • Server 還沒回過來,馬上再打一次,等於雪上加霜
  • 頻繁重試反而加重服務負擔
  • 看起來像 DDoS 攻擊 XD

指數退避的邏輯:

  • 第 1 次失敗,等 2 秒再試
  • 第 2 次失敗,等 4 秒再試
  • 第 3 次失敗,等 8 秒再試
  • 以此類推…

給 server 喘息的時間,機率就高很多。

通用模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public boolean retry(int maxAttempts, int initialDelaySeconds, Runnable action) {
int attempts = 0;
int delay = initialDelaySeconds;

while (attempts < maxAttempts) {
try {
attempts++;
action.run();
return true; // 成功就返回
} catch (Exception e) {
logger.error("Attempt {}/{} failed: {}", attempts, maxAttempts, e.getMessage());

if (attempts >= maxAttempts) {
break; // 已經到最大嘗試次數,放棄
}

try {
TimeUnit.SECONDS.sleep(delay);
delay *= 2; // 指數增長
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // 恢復 interrupt 狀態
break;
}
}
}

return false; // 全部失敗
}

怎麼用

最常見的場景就是打 HTTP 請求:

1
2
3
4
5
6
7
8
9
10
11
12
retry(3, 2, () -> {
ResponseEntity<String> response = restTemplate.exchange(
"https://api.example.com/data",
HttpMethod.GET,
null,
String.class
);

if (response.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException("Request failed");
}
});

或者是資料庫操作:

1
2
3
retry(5, 1, () -> {
userRepository.save(user);
});

要注意的坑

1. Delay 上限要加

指數增長如果不加上限,到第 10 次重試可能要等 1024 秒(快 17 分鐘了)。加個 cap:

1
delay = Math.min(delay * 2, 60);  // 最多等 60 秒

2. InterruptedException 要處理

當 Thread 被 interrupt 時,sleep() 會拋出 InterruptedException,要記得呼叫 Thread.currentThread().interrupt() 恢復狀態,不然其他地方檢查不到 interrupt flag。

3. 並不是所有 Exception 都該重試

網路超時、連線拒絕可以重試,但如果是參數錯誤、權限不足,重試再多次也沒用。可以加個 filter:

1
2
3
4
5
6
7
8
public boolean retry(int maxAttempts, int initialDelaySeconds, 
Runnable action, Predicate<Exception> retryable) {
// ... 在 catch 的時候加上判斷
if (!retryable.test(e)) {
throw e; // 不該重試,直接丟出去
}
// ... 繼續重試邏輯
}

4. Jitter 增加隨機性

在真實環境中,如果有多個客戶端同時 retry,都在相同的時間點同時重試,又會造成新的尖峰。可以加點隨機延遲(jitter):

1
delay = (int)(Math.random() * initialDelaySeconds);  // 亂數退避

實務建議

  • 基本的初始延遲設 2 秒,最多重試 3 次
  • API 呼叫用 10 秒、重試 5 次(因為網路不穩)
  • 資料庫操作用 1 秒、重試 3 次
  • 加日誌,這樣遇到問題才看得出來是 retry 成功還是全部爆了

這個模版夠通用,大部分場景 copy-paste 就能用。踩過的坑就寫在上面了,應該不會再踩一次。