接口重試的7種常用方案詳細介紹
前言
記得五年前的一個深夜,某個電商平臺的訂單退款接口突發(fā)異常,因為銀行系統(tǒng)網(wǎng)絡(luò)抖動,退款請求連續(xù)失敗。
原本技術(shù)團隊只是想“好心重試幾次”,結(jié)果開發(fā)小哥寫的重試代碼竟瘋狂調(diào)用了銀行的退款接口 82次!
最終導(dǎo)致用戶賬戶重復(fù)退款,平臺損失過百萬。
老板在復(fù)盤會上質(zhì)問:“接口重試這么基礎(chǔ)的事,為什么還能捅出大簍子?”
大家啞口無言,因為所有人都以為只要加個 for
循環(huán),再睡幾秒就完事了……
這篇文章跟大家一起聊聊重試的7種常用方案,希望對你會有所幫助。
1 暴力輪回法
問題場景
某實習(xí)生寫的用戶注冊短信發(fā)送接口。
在一個while循環(huán)中,重復(fù)調(diào)用第三方的發(fā)短信接口給用戶發(fā)送短信。
代碼如下:
public void sendSms(String phone) { int retry = 0; while (retry < 5) { // 無腦循環(huán) try { smsClient.send(phone); break; } catch (Exception e) { retry++; Thread.sleep(1000); // 固定1秒睡眠 } } }
事故現(xiàn)場
某次短信服務(wù)器出現(xiàn)了過載問題,導(dǎo)致所有請求都延遲了3秒。
這個暴力循環(huán)的代碼在 0.5秒內(nèi)同時發(fā)起數(shù)萬次重試,直接打爆短信平臺,觸發(fā)了 熔斷封禁,連正常請求也被拒絕。
教訓(xùn)
- ?? 不做延遲間隔調(diào)整:固定間隔導(dǎo)致重試請求集中爆發(fā)
- ?? 無視異常類型:非臨時性錯誤(如參數(shù)錯誤)也嘗試重試
- ?? 修復(fù)方案:加上隨機的重試間隔,并過濾不可重試的異常
2 Spring Retry
應(yīng)用場景
Spring Retry適用于中小項目,通過注解快速實現(xiàn)基本重試和熔斷(如訂單狀態(tài)查詢接口)。
通過聲明@Retryable注解,來實現(xiàn)接口重試的功能。
配置示例
@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; }
優(yōu)勢
- 聲明式注解:代碼簡潔,與業(yè)務(wù)邏輯解耦
- 指數(shù)退避:自動拉長重試間隔
- 熔斷集成:結(jié)合
@CircuitBreaker
可快速阻斷異常流量
3 Resilience4j
高階場景
對于有些需要自定義退避算法、熔斷策略和多層防護的大中型系統(tǒng)(如支付核心接口),我們可以使用 Resilience4j。
核心代碼如下:
// 1. 重試配置:指數(shù)退避 + 隨機抖動 RetryConfig retryConfig = RetryConfig.custom() .maxAttempts(3) .intervalFunction(IntervalFunction.ofExponentialRandomBackoff( 1000L, // 初始間隔1秒 2.0, // 指數(shù)倍數(shù) 0.3 // 隨機抖動系數(shù) )) .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); // 執(zhí)行業(yè)務(wù)邏輯 Supplier<Boolean> supplier = () -> paymentService.pay(); Supplier<Boolean> decorated = Decorators.ofSupplier(supplier) .withRetry(retry) .withCircuitBreaker(cb) .decorate();
效果
某電商大廠上線此方案后,支付接口 超時率下降60% ,且熔斷觸發(fā)頻率降低近 90%
真正做到了“打不還手,罵不還口”。
4 MQ隊列
適用場景
高并發(fā)、允許延時的異步場景(如物流狀態(tài)同步)。
實現(xiàn)原理
- 首次請求失敗后,將消息投遞至 延時隊列
- 隊列根據(jù)預(yù)設(shè)的延時時間(如5秒、30秒、1分鐘)重試消費
- 若達到最大重試次數(shù),則轉(zhuǎn)存至 死信隊列(人工處理)
RocketMQ代碼片段如下:
// 生產(chǎn)者發(fā)送延時消息 Message<String> message = new Message(); message.setBody("訂單數(shù)據(jù)"); message.setDelayTimeLevel(3); // RocketMQ預(yù)設(shè)的10秒延遲級別 rocketMQTemplate.send(message); // 消費者重試 @RocketMQMessageListener(topic = "DELAY_TOPIC") public class DelayConsumer { @Override public void handleMessage(Message message) { try { syncLogistics(message); } catch (Exception e) { // 重試次數(shù) + 1,并重新發(fā)送到更高延遲級別 resendWithDelay(message, retryCount + 1); } } }
如何RocketMQ的消費者消費失敗,會自動發(fā)起重試。
5 定時任務(wù)
適用場景
對于有些不需要實時反饋,允許批量處理的任務(wù)(如文件導(dǎo)入)的業(yè)務(wù)場景,我們可以使用定時任務(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 兩階段提交
適用場景
對于嚴格保證數(shù)據(jù)一致性的場景(如資金轉(zhuǎn)賬),我們可以使用兩階段提交機制。
關(guān)鍵實現(xiàn)
- 第一階段:記錄操作流水到數(shù)據(jù)庫(狀態(tài)為“進行中”)
- 第二階段:調(diào)用遠程接口,并根據(jù)結(jié)果更新流水狀態(tài)
- 定時補償:掃描超時的“進行中”流水重新提交
大致代碼如下:
@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)用場景
對于一些多服務(wù)實例、多線程環(huán)境的防重復(fù)提交(如秒殺)的業(yè)務(wù)場景,我們可以使用分布式鎖。
這里以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é)
重試就像機房里的滅火器——永遠不希望用到它,但必須保證關(guān)鍵時刻能救命。
我們工作中選擇哪種方案?
別只看技術(shù)潮流,而要看業(yè)務(wù)的長矛和盾牌,需要哪種配合。
最后送大家一句話:系統(tǒng)穩(wěn)定的秘訣,是永遠對重試保持敬畏。
到此這篇關(guān)于接口重試的7種常用方案詳細介紹的文章就介紹到這了,更多相關(guān)接口重試方法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決springboot 連接 mysql 時報錯 using password: NO的方案
在本篇文章里小編給大家整理了關(guān)于解決springboot 連接 mysql 時報錯 using password: NO的方案,有需要的朋友們可以學(xué)習(xí)下。2020-01-01Spring Data MongoDB 數(shù)據(jù)庫批量操作的方法
在項目開發(fā)中經(jīng)常會批量插入數(shù)據(jù)和更新數(shù)據(jù)的操作,這篇文章主要介紹了Spring Data MongoDB 數(shù)據(jù)庫批量操作的方法,非常具有實用價值,需要的朋友可以參考下2018-11-11解決java.sql.SQLException:The?server?time?zone?value?&apo
這篇文章主要介紹了解決java.sql.SQLException:The?server?time?zone?value?'?D1ú±ê×?ê±??'?is?unrecognized問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03SpringBoot整合H2數(shù)據(jù)庫的操作方法
H2是一個Java語言編寫的嵌入式數(shù)據(jù)庫,它不受平臺的限制,同時H2提供了一個十分方便的web控制臺,用于操作和管理數(shù)據(jù)庫內(nèi)容,本文介紹SpringBoot整合H2數(shù)據(jù)庫的方法,感興趣的朋友一起看看吧2024-01-01