前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >瞧瞧别人家的接口重试,那叫一个优雅!

瞧瞧别人家的接口重试,那叫一个优雅!

作者头像
苏三说技术
发布2025-03-11 21:15:48
发布2025-03-11 21:15:48
3500
代码可运行
举报
文章被收录于专栏:苏三说技术苏三说技术
运行总次数:0
代码可运行

大家好,我是苏三,又跟大家见面了。

前言

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

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

最终导致用户账户重复退款,平台损失过百万。

老板在复盘会上质问:“接口重试这么基础的事,为什么还能捅出大篓子?”

大家哑口无言,因为所有人都以为只要加个 for 循环,再睡几秒就完事了……

这篇文章跟大家一起聊聊重试的7种常用方案,希望对你会有所帮助。

1 暴力轮回法

问题场景

某实习生写的用户注册短信发送接口。

在一个while循环中,重复调用第三方的发短信接口给用户发送短信。

代码如下:

代码语言:javascript
代码运行次数:0
复制
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注解,来实现接口重试的功能。

配置示例

代码语言:javascript
代码运行次数:0
复制
@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。

核心代码如下:

代码语言:javascript
代码运行次数:0
复制
// 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代码片段如下:

代码语言:javascript
代码运行次数:0
复制
// 生产者发送延时消息
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为例。

具体代码如下:

代码语言:javascript
代码运行次数:0
复制
@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. 定时补偿:扫描超时的“进行中”流水重新提交

大致代码如下:

代码语言:javascript
代码运行次数:0
复制
@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的分布式锁为例。

代码如下:

代码语言:javascript
代码运行次数:0
复制
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;
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 苏三说技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 大家好,我是苏三,又跟大家见面了。
  • 前言
  • 1 暴力轮回法
    • 问题场景
    • 事故现场
    • 教训
  • 2 Spring Retry
    • 应用场景
    • 配置示例
    • 优势
  • 3 Resilience4j
    • 高阶场景
    • 效果
  • 4 MQ队列
    • 适用场景
    • 实现原理
  • 5 定时任务
    • 适用场景
  • 6 两阶段提交
    • 适用场景
    • 关键实现
  • 7 分布式锁
    • 应用场景
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档