找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

查看: 95|回复: 0

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

[复制链接]

42

主题

5

回帖

54

积分

初中生

热心值
0
IT币
261
贡献值
0
发表于 2025-4-16 17:17:02 | 显示全部楼层 |阅读模式
前言
记得五年前的一个深夜,某个电商平台的订单退款接口突发异常,因为银行系统网络抖动,退款请求连续失败。
原本技术团队只是想"好心重试几次",结果开发小哥写的重试代码竟疯狂调用了银行的退款接口82次!
最终导致用户账户重复退款,平台损失过百万。
老板在复盘会上质问:"接口重试这么基础的事,为什么还能捅出大篓子?"
大家哑口无言,因为所有人都以为只要加个for循环,再睡几秒就完事了……
这篇文章跟大家一起聊聊重试的7种常用方案,希望对你会有所帮助。

1 暴力轮回法问题场景
某实习生写的用户注册短信发送接口。
在一个while循环中,重复调用第三方的发短信接口给用户发送短信
代码如下:
  • typescript
  • public void sendSms(String phone) {
  •     int retry = 0;
  •     while (retry < 5) { // 无脑循环
  •         try {
  •             smsClient.send(phone);
  •             break;
  •         } catch (Exception e) {
  •             retry++;
  •             Thread.sleep(1000); // 固定1秒睡眠
  •         }
  •     }
  • }

[color=#336699 !important]复制代码

事故现场
某次短信服务器出现了过载问题,导致所有请求都延迟了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;
  • }

[color=#336699 !important]复制代码


优势
  • 声明式注解:代码简洁,与业务逻辑解耦
  • 指数退避:自动拉长重试间隔
  • 熔断集成:结合@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();

[color=rgb(51, 102, 153) !important]复制代码

效果
某电商大厂上线此方案后,支付接口超时率下降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);
  •         }
  •     }
  • }

[color=rgb(51, 102, 153) !important]复制代码

如果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);
  •     });
  • }

[color=rgb(51, 102, 153) !important]复制代码


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);
    •     }
    • }

    [color=rgb(51, 102, 153) !important]复制代码




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;
  • }

[color=rgb(51, 102, 153) !important]复制代码


总结
重试就像机房里的灭火器——永远不希望用到它,但必须保证关键时刻能救命。
我们工作中选择哪种方案?
别只看技术潮流,而要看业务的长矛和盾牌,需要哪种配合。
最后送大家一句话:系统稳定的秘诀,是永远对重试保持敬畏。
转自:苏三说技术

ITbang.Net是一个IT教程分享社区!

寻找论坛资源请善用论坛搜索功能,这样会为你节约不少学习时间;

论坛资源如有过期链接失效等,请到教程反馈区发帖反馈,我们会为您良好的行为点赞加分!

回复

使用道具 举报

*滑块验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

免责声明:
IT帮论坛所发布的一切视频资源、工具软件和网络技术相关的文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该资源,请支持正版软件,购买注册,得到更好的正版服务。

Mail To:Service@ITbang.Net

QQ|Archiver|手机版|小黑屋|IT帮社区 ( 冀ICP备19002104号-2 )

GMT+8, 2025-5-3 22:44 , Processed in 0.074281 second(s), 21 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表