瞧瞧別人家的接口重試,那叫一個(gè)優(yōu)雅!
2025年某電商平臺(tái)深夜故障,因重試策略不當(dāng)導(dǎo)致銀行退款接口被調(diào)用82次,引發(fā)重復(fù)退款126萬(wàn)元!
復(fù)盤(pán)發(fā)現(xiàn):80%的開(kāi)發(fā)者認(rèn)為重試就是for循環(huán)+Thread.sleep()
,卻忽略了重試風(fēng)暴、冪等性缺失、資源雪崩等致命問(wèn)題。
這篇文章跟大家一起聊聊接口重試的8種常用方案,希望對(duì)你會(huì)有所幫助。
一、重試機(jī)制的原因
1.為什么需要重試?
臨時(shí)性故障占比超70%,合理重試可將成功率提升至99%以上。
2.重試的三大陷阱
- 重試風(fēng)暴:固定間隔重試引發(fā)請(qǐng)求洪峰(如萬(wàn)次重試壓垮服務(wù))
- 數(shù)據(jù)不一致:非冪等操作導(dǎo)致重復(fù)生效(如重復(fù)扣款)
- 鏈路阻塞:長(zhǎng)時(shí)重試耗盡線程資源(如數(shù)據(jù)庫(kù)連接池枯竭)
二、基礎(chǔ)重試方案
1.暴力輪回法(青銅)
問(wèn)題代碼:
// 危險(xiǎn)!切勿直接用于生產(chǎn)!
public void sendSms(String phone) {
int retry = 0;
while (retry < 5) {
try {
smsClient.send(phone);
break;
} catch (Exception e) {
retry++;
Thread.sleep(1000); // 固定1秒間隔
}
}
}
事故案例:某平臺(tái)短信接口重試風(fēng)暴,觸發(fā)第三方熔斷封禁。
優(yōu)化方向:增加隨機(jī)抖動(dòng) + 異常過(guò)濾。
2.Spring Retry(黃金)
聲明式注解控制重試:
@Retryable(
value = {TimeoutException.class}, // 僅重試超時(shí)異常
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2) // 指數(shù)退避:1s→2s→4s
)
public boolean queryOrder(String orderId) {
return httpClient.get("/order/" + orderId);
}
@Recover // 兜底降級(jí)
public boolean fallback(TimeoutException e) {
return false;
}
優(yōu)勢(shì):
- 注解驅(qū)動(dòng),業(yè)務(wù)零侵入
- 支持指數(shù)退避策略
- 無(wú)縫集成熔斷器@CircuitBreaker
三、高階重試方案
1.Resilience4j(白金)
應(yīng)對(duì)高并發(fā)場(chǎng)景的重試+熔斷組合拳:
// 重試配置:指數(shù)退避+隨機(jī)抖動(dòng)
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3)
.intervalFunction(IntervalFunction.ofExponentialRandomBackoff(
1000L, 2.0, 0.3// 初始1s,指數(shù)倍率2,抖動(dòng)率30%
))
.retryOnException(e -> e instanceof TimeoutException)
.build();
// 熔斷配置:錯(cuò)誤率超50%觸發(fā)熔斷
CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
.slidingWindow(10, 10, COUNT_BASED)
.failureRateThreshold(50)
.build();
// 組合裝飾
Supplier<Boolean> supplier = () -> paymentService.pay();
Supplier<Boolean> decorated = Decorators.ofSupplier(supplier)
.withRetry(Retry.of("payment", retryConfig))
.withCircuitBreaker(CircuitBreaker.of("payment", cbConfig))
.decorate();
效果:某支付系統(tǒng)接入后超時(shí)率下降60%,熔斷觸發(fā)率降低90%
2.Guava-Retrying(鉆石)
靈活定制復(fù)雜重試邏輯:
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.equalTo(false)) // 返回false重試
.retryIfExceptionOfType(IOException.class)
.withWaitStrategy(WaitStrategies.exponentialWait(1000, 30, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
.build();
retryer.call(() -> uploadService.upload(file)); // 執(zhí)行
核心能力:
- 支持結(jié)果/異常雙模式觸發(fā)
- 提供7種等待策略(隨機(jī)、指數(shù)、遞增等)
- 可監(jiān)聽(tīng)每次重試事件
四、分布式重試方案
1.MQ延時(shí)隊(duì)列(星耀Ⅰ)
適用場(chǎng)景:異步解耦的高并發(fā)系統(tǒng)(如物流狀態(tài)同步)
架構(gòu)原理:
RocketMQ實(shí)現(xiàn):
// 生產(chǎn)者發(fā)送延時(shí)消息
Message msg = new Message();
msg.setBody(orderData);
msg.setDelayTimeLevel(3); // RocketMQ預(yù)設(shè)10秒延遲
rocketMQTemplate.send(msg);
// 消費(fèi)者
@RocketMQMessageListener(topic = "RETRY_TOPIC")
publicclass RetryConsumer {
public void consume(Message msg) {
try {
process(msg);
} catch (Exception e) {
// 提升延遲級(jí)別重發(fā)
msg.setDelayTimeLevel(5);
resend(msg);
}
}
}
優(yōu)勢(shì):
- 重試與業(yè)務(wù)邏輯解耦
- 天然支持梯度延時(shí)
- 死信隊(duì)列兜底人工處理
2.定時(shí)任務(wù)補(bǔ)償(星耀Ⅱ)
適用場(chǎng)景:允許延遲的批處理任務(wù)(如文件導(dǎo)入)
@Scheduled(cron = "0 0/5 * * * ?") // 每5分鐘執(zhí)行
public void retryFailedTasks() {
List<FailedTask> tasks = taskDao.findFailed(MAX_RETRY);
tasks.forEach(task -> {
if (retry(task)) {
task.markSuccess();
} else {
task.incrRetryCount();
}
taskDao.update(task);
});
}
關(guān)鍵點(diǎn):
- 數(shù)據(jù)庫(kù)記錄失敗任務(wù)
- 低峰期批量處理
- 獨(dú)立線程池隔離資源
3.兩階段提交(王者Ⅰ)
金融級(jí)一致性保障(如轉(zhuǎn)賬):
@Transactional
public void transfer(TransferRequest req) {
// 階段1:持久化操作流水
TransferRecord record = recordDao.create(req, PENDING);
// 階段2:調(diào)用銀行接口
boolean success = bankClient.transfer(req);
// 更新?tīng)顟B(tài)
recordDao.updateStatus(record.getId(), success ? SUCCESS : FAILED);
if (!success) {
mqTemplate.send("TRANSFER_RETRY_QUEUE", req); // 觸發(fā)異步重試
}
}
// 補(bǔ)償任務(wù)(掃描掛起流水)
@Scheduled(fixedRate = 30000)
public void compensate() {
List<TransferRecord> pendings = recordDao.findPending(30);
pendings.forEach(this::retryTransfer);
}
核心思想:操作前先留痕,任何失敗可追溯
4.分布式鎖重試(王者Ⅱ)
防重復(fù)提交終極方案(如秒殺):
public boolean retryWithLock(String key, int maxRetry) {
String lockKey = "RETRY_LOCK:" + key;
for (int i = 0; i < maxRetry; i++) {
if (redis.setIfAbsent(lockKey, "1", 30, SECONDS)) {
try {
return callApi(); // 持有鎖時(shí)執(zhí)行
} finally {
redis.delete(lockKey);
}
}
Thread.sleep(1000 * (i + 1)); // 等待鎖釋放
}
return false;
}
適用場(chǎng)景:
- 多實(shí)例部署環(huán)境
- 高競(jìng)爭(zhēng)資源訪問(wèn)
- 等冪性要求極高業(yè)務(wù)
五、響應(yīng)式重試:Spring WebFlux方案
1.響應(yīng)式重試操作符
Mono<String> remoteCall = Mono.fromCallable(() -> {
if (Math.random() > 0.5) throw new RuntimeException("模擬失敗");
return "Success";
});
remoteCall.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.doBeforeRetry(signal -> log.warn("第{}次重試", signal.totalRetries()))
.subscribe();
策略支持:
- 指數(shù)退避:
Retry.backoff(maxAttempts, firstBackoff)
- 隨機(jī)抖動(dòng):
.jitter(0.5)
- 條件過(guò)濾:
.filter(ex -> ex instanceof TimeoutException)
六、重試的避坑指南
1.必須實(shí)現(xiàn)的三大防護(hù)
防護(hù)類(lèi)型 | 目的 | 實(shí)現(xiàn)方案 |
冪等性防護(hù) | 防止重復(fù)生效 | 唯一ID+狀態(tài)機(jī) |
重試風(fēng)暴防護(hù) | 避免洪峰沖擊 | 指數(shù)退避+隨機(jī)抖動(dòng) |
資源隔離 | 保護(hù)主鏈路資源 | 線程池隔離/熔斷器 |
2.經(jīng)典踩坑案例
- 坑1:無(wú)限制重試→ 某系統(tǒng)因未設(shè)重試上限,線程池爆滿(mǎn)導(dǎo)致集群雪崩
- 解法:
maxAttempts=3
+ 熔斷降級(jí) - 坑2:忽略錯(cuò)誤類(lèi)型→ 參數(shù)錯(cuò)誤(4xx)被反復(fù)重試,放大無(wú)效流量
- 解法:
retryOnException(e -> e instanceof TimeoutException)
- 坑3:上下文丟失→ 異步重試后丟失用戶(hù)會(huì)話(huà)信息
- 解法:重試前快照關(guān)鍵上下文(如userId、requestId)
七、方案選型參考圖
總結(jié)
- 敬畏每一次重試:重試不是暴力補(bǔ)救,而是精密流量控制。
- 面向失敗設(shè)計(jì):假設(shè)網(wǎng)絡(luò)不可靠、服務(wù)會(huì)宕機(jī)、資源終將枯竭。
- 分層防御體系:
a.代碼層:冪等性 + 超時(shí)控制
b.框架層:退避策略 + 熔斷降級(jí)
c.架構(gòu)層:異步解耦 + 持久化補(bǔ)償
- 沒(méi)有銀彈:秒殺場(chǎng)景用分布式鎖,支付系統(tǒng)用兩階段提交,IoT設(shè)備用MQTT重試機(jī)制。
正如分布式系統(tǒng)大師Leslie Lamport所言:“重試是分布式系統(tǒng)的成人禮”。
掌握這8種方案,你將擁有讓系統(tǒng)“起死回生”的魔法!