瞧瞧別人家的接口重試,那叫一個(gè)優(yōu)雅!
前言
記得五年前的一個(gè)深夜,某個(gè)電商平臺(tái)的訂單退款接口突發(fā)異常,因?yàn)殂y行系統(tǒng)網(wǎng)絡(luò)抖動(dòng),退款請(qǐng)求連續(xù)失敗。
原本技術(shù)團(tuán)隊(duì)只是想“好心重試幾次”,結(jié)果開(kāi)發(fā)小哥寫(xiě)的重試代碼竟瘋狂調(diào)用了銀行的退款接口 82次!
最終導(dǎo)致用戶賬戶重復(fù)退款,平臺(tái)損失過(guò)百萬(wàn)。
老板在復(fù)盤(pán)會(huì)上質(zhì)問(wèn):“接口重試這么基礎(chǔ)的事,為什么還能捅出大簍子?”
大家啞口無(wú)言,因?yàn)樗腥硕家詾橹灰觽€(gè) for 循環(huán),再睡幾秒就完事了……
這篇文章跟大家一起聊聊重試的7種常用方案,希望對(duì)你會(huì)有所幫助。
1.暴力輪回法
問(wèn)題場(chǎng)景
某實(shí)習(xí)生寫(xiě)的用戶注冊(cè)短信發(fā)送接口。
在一個(gè)while循環(huán)中,重復(fù)調(diào)用第三方的發(fā)短信接口給用戶發(fā)送短信。
代碼如下:
public void sendSms(String phone) {
int retry = 0;
while (retry < 5) { // 無(wú)腦循環(huán)
try {
smsClient.send(phone);
break;
} catch (Exception e) {
retry++;
Thread.sleep(1000); // 固定1秒睡眠
}
}
}
事故現(xiàn)場(chǎng)
某次短信服務(wù)器出現(xiàn)了過(guò)載問(wèn)題,導(dǎo)致所有請(qǐng)求都延遲了3秒。
這個(gè)暴力循環(huán)的代碼在 0.5秒內(nèi)同時(shí)發(fā)起數(shù)萬(wàn)次重試,直接打爆短信平臺(tái),觸發(fā)了 熔斷封禁,連正常請(qǐng)求也被拒絕。
教訓(xùn)
- ?? 不做延遲間隔調(diào)整:固定間隔導(dǎo)致重試請(qǐng)求集中爆發(fā)
- ?? 無(wú)視異常類(lèi)型:非臨時(shí)性錯(cuò)誤(如參數(shù)錯(cuò)誤)也嘗試重試
- ?? 修復(fù)方案:加上隨機(jī)的重試間隔,并過(guò)濾不可重試的異常
2.Spring Retry
應(yīng)用場(chǎng)景
Spring Retry適用于中小項(xiàng)目,通過(guò)注解快速實(shí)現(xiàn)基本重試和熔斷(如訂單狀態(tài)查詢接口)。
通過(guò)聲明@Retryable注解,來(lái)實(shí)現(xiàn)接口重試的功能。
配置示例
@Retryable(
value = {TimeoutException.class}, // 只重試超時(shí)異常
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;
}
優(yōu)勢(shì)
- 聲明式注解:代碼簡(jiǎn)潔,與業(yè)務(wù)邏輯解耦
- 指數(shù)退避:自動(dòng)拉長(zhǎng)重試間隔
- 熔斷集成:結(jié)合 @CircuitBreaker 可快速阻斷異常流量
3.Resilience4j
高階場(chǎng)景
對(duì)于有些需要自定義退避算法、熔斷策略和多層防護(hù)的大中型系統(tǒng)(如支付核心接口),我們可以使用 Resilience4j。
核心代碼如下:
// 1. 重試配置:指數(shù)退避 + 隨機(jī)抖動(dòng)
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3)
.intervalFunction(IntervalFunction.ofExponentialRandomBackoff(
1000L, // 初始間隔1秒
2.0, // 指數(shù)倍數(shù)
0.3 // 隨機(jī)抖動(dòng)系數(shù)
))
.retryOnException(e -> e instanceof TimeoutException)
.build();
// 2. 熔斷配置:錯(cuò)誤率超50%時(shí)熔斷
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);
// 執(zhí)行業(yè)務(wù)邏輯
Supplier<Boolean> supplier = () -> paymentService.pay();
Supplier<Boolean> decorated = Decorators.ofSupplier(supplier)
.withRetry(retry)
.withCircuitBreaker(cb)
.decorate();
效果
某電商大廠上線此方案后,支付接口 超時(shí)率下降60% ,且熔斷觸發(fā)頻率降低近 90%
真正做到了“打不還手,罵不還口”。
4.MQ隊(duì)列
適用場(chǎng)景
高并發(fā)、允許延時(shí)的異步場(chǎng)景(如物流狀態(tài)同步)。
實(shí)現(xiàn)原理
- 首次請(qǐng)求失敗后,將消息投遞至 延時(shí)隊(duì)列
- 隊(duì)列根據(jù)預(yù)設(shè)的延時(shí)時(shí)間(如5秒、30秒、1分鐘)重試消費(fèi)
- 若達(dá)到最大重試次數(shù),則轉(zhuǎn)存至 死信隊(duì)列(人工處理)
RocketMQ代碼片段如下:
// 生產(chǎn)者發(fā)送延時(shí)消息
Message<String> message = new Message();
message.setBody("訂單數(shù)據(jù)");
message.setDelayTimeLevel(3); // RocketMQ預(yù)設(shè)的10秒延遲級(jí)別
rocketMQTemplate.send(message);
// 消費(fèi)者重試
@RocketMQMessageListener(topic = "DELAY_TOPIC")
public class DelayConsumer {
@Override
public void handleMessage(Message message) {
try {
syncLogistics(message);
} catch (Exception e) {
// 重試次數(shù) + 1,并重新發(fā)送到更高延遲級(jí)別
resendWithDelay(message, retryCount + 1);
}
}
}
如何RocketMQ的消費(fèi)者消費(fèi)失敗,會(huì)自動(dòng)發(fā)起重試。
5.定時(shí)任務(wù)
適用場(chǎng)景
對(duì)于有些不需要實(shí)時(shí)反饋,允許批量處理的任務(wù)(如文件導(dǎo)入)的業(yè)務(wù)場(chǎng)景,我們可以使用定時(shí)任務(wù)。
在這里以Quartz為例。
具體代碼如下:
@Scheduled(cron = "0 0/5 * * * ?") // 每5分鐘執(zhí)行
public void retryFailedTasks() {
List<FailedTask> list = failedTaskDao.listUnprocessed(5); // 查失敗任務(wù)
list.forEach(task -> {
try {
retryTask(task);
task.markSuccess();
} catch (Exception e) {
task.incrRetryCount();
}
failedTaskDao.update(task);
});
}
6.兩階段提交
適用場(chǎng)景
對(duì)于嚴(yán)格保證數(shù)據(jù)一致性的場(chǎng)景(如資金轉(zhuǎn)賬),我們可以使用兩階段提交機(jī)制。
關(guān)鍵實(shí)現(xiàn)
- 第一階段:記錄操作流水到數(shù)據(jù)庫(kù)(狀態(tài)為“進(jìn)行中”)
- 第二階段:調(diào)用遠(yuǎn)程接口,并根據(jù)結(jié)果更新流水狀態(tài)
- 定時(shí)補(bǔ)償:掃描超時(shí)的“進(jìn)行中”流水重新提交
大致代碼如下:
@Transactional
public void transfer(TransferRequest req) {
// 1. 記錄流水
transferRecordDao.create(req, PENDING);
// 2. 調(diào)用銀行接口
boolean success = bankClient.transfer(req);
// 3. 更新流水狀態(tài)
transferRecordDao.updateStatus(req.getId(), success ? SUCCESS : FAILED);
// 4. 失敗轉(zhuǎn)異步重試
if (!success) {
mqTemplate.send("TRANSFER_RETRY_QUEUE", req);
}
}
7.分布式鎖
應(yīng)用場(chǎng)景
對(duì)于一些多服務(wù)實(shí)例、多線程環(huán)境的防重復(fù)提交(如秒殺)的業(yè)務(wù)場(chǎng)景,我們可以使用分布式鎖。
這里以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;
}
總結(jié)
重試就像機(jī)房里的滅火器——永遠(yuǎn)不希望用到它,但必須保證關(guān)鍵時(shí)刻能救命。
我們工作中選擇哪種方案?
別只看技術(shù)潮流,而要看業(yè)務(wù)的長(zhǎng)矛和盾牌,需要哪種配合。
最后送大家一句話:系統(tǒng)穩(wěn)定的秘訣,是永遠(yuǎn)對(duì)重試保持敬畏。