前言
记得五年前的一个深夜,某个电商平台的订单退款接口突发异常,因为银行系统网络抖动,退款请求连续失败。
原本技术团队只是想“好心重试几次”,结果开发小哥写的重试代码竟疯狂调用了银行的退款接口 82次!
最终导致用户账户重复退款,平台损失过百万。
老板在复盘会上质问:“接口重试这么基础的事,为什么还能捅出大篓子?”
大家哑口无言,因为所有人都以为只要加个 for 循环,再睡几秒就完事了……
这篇文章跟大家一起聊聊重试的7种常用方案,希望对你会有所帮助。
1 暴力轮回法
问题场景
某实习生写的用户注册短信发送接口。
在一个while循环中,重复调用第三方的发短信接口给用户发送短信。
代码如下:
public void sendsms(string phone) {
int retry = 0;
while (retry < 5) { // 无脑循环
try {
smsclient.send(phone);
break;
} catch (exception e) {
retry++;
thread.sleep(1000); // 固定1秒睡眠
}
}
}
事故现场
某次短信服务器出现了过载问题,导致所有请求都延迟了3秒。
这个暴力循环的代码在 0.5秒内同时发起数万次重试,直接打爆短信平台,触发了 熔断封禁,连正常请求也被拒绝。
教训
- 💥 不做延迟间隔调整:固定间隔导致重试请求集中爆发
- 💥 无视异常类型:非临时性错误(如参数错误)也尝试重试
- 🔑 修复方案:加上随机的重试间隔,并过滤不可重试的异常
2 spring retry
应用场景
spring retry适用于中小项目,通过注解快速实现基本重试和熔断(如订单状态查询接口)。
通过声明@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
高阶场景
对于有些需要自定义退避算法、熔断策略和多层防护的大中型系统(如支付核心接口),我们可以使用 resilience4j。
核心代码如下:
// 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();
效果
某电商大厂上线此方案后,支付接口 超时率下降60% ,且熔断触发频率降低近 90%
真正做到了“打不还手,骂不还口”。
4 mq队列
适用场景
高并发、允许延时的异步场景(如物流状态同步)。
实现原理
- 首次请求失败后,将消息投递至 延时队列
- 队列根据预设的延时时间(如5秒、30秒、1分钟)重试消费
- 若达到最大重试次数,则转存至 死信队列(人工处理)
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 两阶段提交
适用场景
对于严格保证数据一致性的场景(如资金转账),我们可以使用两阶段提交机制。
关键实现
- 第一阶段:记录操作流水到数据库(状态为“进行中”)
- 第二阶段:调用远程接口,并根据结果更新流水状态
- 定时补偿:扫描超时的“进行中”流水重新提交
大致代码如下:
@transactional
public void transfer(transferrequest req) {
// 1. 记录流水
transferrecorddao.create(req, pending);
// 2. 调用银行接口
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;
}
总结
重试就像机房里的灭火器——永远不希望用到它,但必须保证关键时刻能救命。
我们工作中选择哪种方案?
别只看技术潮流,而要看业务的长矛和盾牌,需要哪种配合。
最后送大家一句话:系统稳定的秘诀,是永远对重试保持敬畏。
到此这篇关于接口重试的7种常用方案详细介绍的文章就介绍到这了,更多相关接口重试方法内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论