上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

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

[复制链接]
  • TA的每日心情
    无聊
    2025-4-11 10:54
  • 签到天数: 51 天

    [LV.5]常住居民I

    88

    主题

    38

    回帖

    282

    积分

    高级工程师

    积分
    282
    发表于 2025-4-16 16:58:35 | 显示全部楼层 | 阅读模式

    前言

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

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

    事故现场

    某次短信服务器出现了过载问题,导致所有请求都延迟了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. 定时补偿:扫描超时的"进行中"流水重新提交
      具体代码如下
      @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;
    }

    总结

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

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

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

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

    转自:苏三说技术

    发表回复

    本版积分规则

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