You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Framework RestClient如何在每次API调用间设置间隔?

问题场景

在Spring Framework中使用org.springframework.web.client.RestClient调用外部API时,遭遇API限流限制(每秒仅允许1次调用),触发报错:

org.springframework.web.client.HttpClientErrorException$TooManyRequests: too many request 2 times in 1000 millseconds

现有核心代码如下:

RestClient配置类

@Configuration
public class CloudConfig {
    @Value("${experimental.host}")
    String baseURI;
    @Bean
    RestClient restClient(RestClient.Builder builder){
        return builder.baseUrl(baseURI).build();
    }
}

API调用服务类

@Component
public class RequestService {

    private final RestClient restClient;

    // 补充原代码遗漏的构造注入
    public RequestService(RestClient restClient) {
        this.restClient = restClient;
    }

    public String callAPI(int userID){
        Map<String, Object> properties = Map.of("id", userID);
        return restClient.post()
                .uri("/external")
                .body(properties)
                .retrieve()
                .body(String.class);
    }
}

定时任务调用逻辑

@Service
public class RabbitMQProducer {

    private final UserRepository repository;
    private final RequestService requestService;

    // 补充原代码遗漏的构造注入
    public RabbitMQProducer(UserRepository repository, RequestService requestService) {
        this.repository = repository;
        this.requestService = requestService;
    }

    @Scheduled(fixedRate = 10000)
    public void sendUserData(){
        for(User user : repository.findAll()) {
            String data = requestService.callAPI(user.getID());
            // 后续处理逻辑
        }
    }   
}

直接使用Thread.sleep(1000)虽能临时规避问题,但并非最优解,以下是更合理的技术方案:

最优解决方案

1. 令牌桶限流(Guava RateLimiter)

采用令牌桶算法精准控制请求速率,比固定sleep更灵活,既能保证长期速率不超限,也能处理允许范围内的突发请求。

实现步骤:

  • 引入Guava依赖(Maven):
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>
  • 修改RequestService集成RateLimiter:
@Component
public class RequestService {

    private final RestClient restClient;
    private final RateLimiter rateLimiter;

    public RequestService(RestClient restClient) {
        this.restClient = restClient;
        // 每秒生成1个令牌,对应每秒1次API调用
        this.rateLimiter = RateLimiter.create(1.0);
    }

    public String callAPI(int userID){
        // 阻塞等待获取令牌,自动控制请求间隔
        rateLimiter.acquire();
        Map<String, Object> properties = Map.of("id", userID);
        return restClient.post()
                .uri("/external")
                .body(properties)
                .retrieve()
                .body(String.class);
    }
}

优势:无需手动处理线程休眠,RateLimiter自动适配请求间隔;若某次API调用耗时超过1秒,后续请求会自动调整间隔,确保整体速率合规;支持配置预热期,适配API的流量突发规则。

2. 限流重试+指数退避

即使做了前置限流,仍可能因网络延迟、API侧计数误差触发限流错误,结合重试+指数退避策略,提升系统容错性。

实现步骤:

  • 引入Spring Retry依赖:
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 启动类添加注解启用重试:
@SpringBootApplication
@EnableRetry
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • 修改RequestService添加重试逻辑:
@Component
@Slf4j
public class RequestService {

    private final RestClient restClient;
    private final RateLimiter rateLimiter;

    public RequestService(RestClient restClient) {
        this.restClient = restClient;
        this.rateLimiter = RateLimiter.create(1.0);
    }

    @Retryable(
            value = {HttpClientErrorException.TooManyRequests.class},
            backoff = @Backoff(delay = 1000, multiplier = 2) // 首次延迟1秒,后续指数递增
    )
    public String callAPI(int userID){
        rateLimiter.acquire();
        Map<String, Object> properties = Map.of("id", userID);
        return restClient.post()
                .uri("/external")
                .body(properties)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
                    if (response.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
                        throw new HttpClientErrorException.TooManyRequests(response.getStatusCode(), response.getStatusText());
                    }
                })
                .body(String.class);
    }

    @Recover
    public String recoverTooManyRequests(HttpClientErrorException.TooManyRequests e, int userID) {
        // 最终重试失败的降级处理:记录日志、标记用户待重试等
        log.error("API调用多次失败,用户ID: {}", userID, e);
        return null;
    }
}

3. 异步延迟队列串行调用

若不希望定时任务线程被阻塞,可将用户任务放入延迟队列,由单线程池按固定间隔执行。

实现步骤:

  • 配置单线程任务调度器:
@Configuration
public class AsyncConfig {
    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(1); // 单线程保证串行执行
        scheduler.setThreadNamePrefix("api-call-scheduler-");
        return scheduler;
    }
}
  • 修改RabbitMQProducer实现延迟调度:
@Service
public class RabbitMQProducer {

    private final UserRepository repository;
    private final RequestService requestService;
    private final ThreadPoolTaskScheduler taskScheduler;

    public RabbitMQProducer(UserRepository repository, RequestService requestService, ThreadPoolTaskScheduler taskScheduler) {
        this.repository = repository;
        this.requestService = requestService;
        this.taskScheduler = taskScheduler;
    }

    @Scheduled(fixedRate = 10000)
    public void sendUserData(){
        List<User> users = repository.findAll();
        for (int i = 0; i < users.size(); i++) {
            User user = users.get(i);
            // 按顺序延迟i*1000毫秒执行,实现每隔1秒调用一次
            taskScheduler.schedule(() -> {
                String data = requestService.callAPI(user.getID());
                // 后续处理逻辑
            }, System.currentTimeMillis() + i * 1000);
        }
    }   
}

优势:定时任务线程无需等待API调用完成,任务在后台串行执行,不影响其他定时任务逻辑。

方案对比
  • 令牌桶限流:最推荐,代码侵入性低,速率控制精准,适配绝大多数场景
  • 重试+退避:作为限流的补充机制,处理突发限流错误,提升系统稳定性
  • 异步延迟队列:适合无需同步等待API结果的场景,避免阻塞主线程

内容的提问来源于stack exchange,提问作者Romillion

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 04:52:05