写点什么

接口重试的 7 种常用方案!

  • 2025-06-03
    福建
  • 本文字数:2927 字

    阅读完需:约 10 分钟

前言


记得五年前的一个深夜,某个电商平台的订单退款接口突发异常,因为银行系统网络抖动,退款请求连续失败。


原本技术团队只是想“好心重试几次”,结果开发小哥写的重试代码竟疯狂调用了银行的退款接口 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 队列


适用场景

高并发、允许延时的异步场景(如物流状态同步)。


实现原理

  1. 首次请求失败后,将消息投递至 延时队列

  2. 队列根据预设的延时时间(如 5 秒、30 秒、1 分钟)重试消费

  3. 若达到最大重试次数,则转存至 死信队列(人工处理)


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 两阶段提交


适用场景

对于严格保证数据一致性的场景(如资金转账),我们可以使用两阶段提交机制。


关键实现

  1. 第一阶段:记录操作流水到数据库(状态为“进行中”)

  2. 第二阶段:调用远程接口,并根据结果更新流水状态

  3. 定时补偿:扫描超时的“进行中”流水重新提交


大致代码如下:

@Transactionalpublic 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;}
复制代码


总结


重试就像机房里的灭火器——永远不希望用到它,但必须保证关键时刻能救命。

我们工作中选择哪种方案?

别只看技术潮流,而要看业务的长矛和盾牌,需要哪种配合。

最后送大家一句话:系统稳定的秘诀,是永远对重试保持敬畏。


文章转载自:苏三说技术

原文链接:https://www.cnblogs.com/12lisu/p/18788852

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
接口重试的7种常用方案!_php_量贩潮汐·WholesaleTide_InfoQ写作社区