优化RestTemplate连接池,提升飞书API性能

Spring 生态下,RestTemplate 默认是没有连接池的(它用的是 JDK 的 HttpURLConnection),如果不配置,每一次请求都会新建连接,这对一天的飞书长周期调用是灾难性的。

我们需要将 RestTemplate 的底层 HTTP 客户端替换为支持池化的实现(Apache HttpClientOkHttp )。下面我以 Apache HttpClient 为例(企业级最稳定),展示如何将"池化思想"完整注入。


第一步:引入依赖(Maven/Gradle)

复制代码
<!-- Apache HttpClient 连接池实现 -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.14</version>
</dependency>

第二步:构建带池化的 RestTemplate(核心配置类)

复制代码
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.TimeUnit;

@Configuration
public class RestTemplatePoolConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(clientHttpRequestFactory());
    }

    private ClientHttpRequestFactory clientHttpRequestFactory() {
        // 1. 创建连接池管理器(核心)
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        
        // 【关键配置】最大总连接数(应对一天内的高并发突发流量)
        connectionManager.setMaxTotal(200);
        
        // 【关键配置】每个路由(即每个域名:open.feishu.cn)的默认最大连接数
        connectionManager.setDefaultMaxPerRoute(50);
        
        // 【可选】针对飞书单独设置更精细的路由限制
        // HttpHost feishuHost = new HttpHost("open.feishu.cn", 443, "https");
        // connectionManager.setMaxPerRoute(new HttpRoute(feishuHost), 80);

        // 2. 构建 HttpClient
        HttpClientBuilder builder = HttpClientBuilder.create();
        builder.setConnectionManager(connectionManager);
        
        // 【池化关键】启用连接池的存活探测(清理僵尸连接)
        builder.evictExpiredConnections();
        builder.evictIdleConnections(30, TimeUnit.SECONDS); // 空闲超过30秒的连接会被回收

        // 3. 包装成 RestTemplate 可识别的 Factory
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(builder.build());
        
        // 【超时配置】防止某个慢请求占住连接池资源不放
        factory.setConnectionRequestTimeout(5000); // 从连接池获取连接的超时时间(池满时等待)
        factory.setConnectTimeout(3000);           // 建立TCP连接的超时
        factory.setReadTimeout(10000);             // 读取飞书响应数据的超时

        return factory;
    }
}

第三步:结合"令牌池/工作池"使用(完整调用示例)

光有连接池还不够,必须配合**信号量(Semaphore)**控制并发数,否则即使连接池有200个连接,也会因为飞书限流(429)产生大量无效重试,浪费资源。

复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

@Service
public class FeishuApiService {

    @Autowired
    private RestTemplate restTemplate; // 使用上面配置好的带池化的实例

    // 工作池(信号量):控制同时只有30个请求在飞书上运行
    private final Semaphore semaphore = new Semaphore(30);

    // 模拟获取飞书用户信息
    public String fetchUser(String userId) {
        // 尝试获取信号量,如果池满则阻塞等待(最多等3秒,避免线程永久挂起)
        try {
            if (!semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
                throw new RuntimeException("系统繁忙,请稍后重试");
            }
            
            // 1. 获取Token(假设从缓存/令牌池取)
            String token = getCachedTenantAccessToken();
            
            // 2. 构造请求
            HttpHeaders headers = new HttpHeaders();
            headers.set("Authorization", "Bearer " + token);
            HttpEntity<String> entity = new HttpEntity<>(headers);
            
            // 3. 发起HTTP请求(连接直接从池中复用)
            String url = "https://open.feishu.cn/open-apis/contact/v3/users/" + userId;
            ResponseEntity<String> response = restTemplate.exchange(
                url, 
                HttpMethod.GET, 
                entity, 
                String.class
            );
            
            // 4. 处理飞书限流(429)------ 释放信号量前处理
            if (response.getStatusCode().is2xxSuccessful()) {
                return response.getBody();
            } else if (response.getStatusCode().value() == 429) {
                // 触发限流,让当前线程休眠退避,不占用信号量
                TimeUnit.SECONDS.sleep(2); 
                // 可以配合重试模板(Spring Retry)重新发起
                return retryWithBackoff(userId);
            }
            return response.getBody();
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            // 【务必释放信号量】无论成功还是异常,都要释放工作池的容量
            semaphore.release();
        }
    }

    private String getCachedTenantAccessToken() {
        // 这里使用本地缓存(如 Caffeine),过期前5分钟异步刷新
        return "fake_token";
    }
}

第四步:针对一天的"数据池"优化(批量聚合)

如果你的场景是拉取10000个用户详情,不要 循环调用上面的 fetchUser。用批量接口 + 分批提交

复制代码
// 数据池思想:每50个ID攒一批,发起一次批量请求
public List<String> fetchUsersBatch(List<String> userIds) {
    List<String> results = new ArrayList<>();
    
    // 将用户ID分成每50个一组(飞书批量接口通常限制最大100)
    List<List<String>> partitions = Lists.partition(userIds, 50);
    
    for (List<String> batch : partitions) {
        // 使用信号量控制并发批次(避免同时发太多批次)
        semaphore.acquire();
        try {
            // 调用飞书的批量查询接口(如果有)
            // POST https://open.feishu.cn/open-apis/contact/v3/users/batch_get
            String response = restTemplate.postForObject(url, batch, String.class);
            results.add(response);
        } finally {
            semaphore.release();
        }
    }
    return results;
}

关键参数调优指南(针对一天的长周期)

参数 推荐值 理由
maxTotal 200 即便有突发流量,200个连接足够;过大反而占用系统文件描述符。
defaultMaxPerRoute 50 飞书单个域名下,50个并发连接已经能跑满大部分API的QPS限额。
evictIdleConnections 30秒 飞书服务端可能主动断开空闲连接,提前清理避免拿到死连接报错。
connectionRequestTimeout 5秒 如果连接池被占满,5秒内拿不到连接就快速失败,避免线程积压。
Semaphore(信号量) 20~30 比连接数略小,提前在业务层限流,降低触发飞书429的概率。

与"令牌池"联动的最佳实践

RestTemplate连接池令牌桶限流器 结合:

复制代码
// 使用 Guava RateLimiter 做令牌池(每秒放行20个请求)
private final RateLimiter rateLimiter = RateLimiter.create(20.0);

// 在调用 RestTemplate 之前
rateLimiter.acquire(); // 阻塞直到获取到令牌
ResponseEntity<String> response = restTemplate.exchange(...);

这样你就有三层防护

  1. 连接池 → 复用TCP连接,降低CPU。

  2. 信号量 → 控制最大并发,防止内存溢出。

  3. 令牌桶 → 平滑QPS,避免触发飞书限流。


避坑提醒

  • 不要 在每次请求时 new RestTemplate(),必须单例注入,否则连接池不生效。

  • 如果你的服务是微服务架构且使用 Spring Cloud Feign ,它底层默认也支持 HttpClient 池化,配置方式类似(设置 feign.httpclient.enabled=true)。

  • 一天的长周期运行,建议监控连接池状态

    PoolingHttpClientConnectionManager cm = ...;
    int leased = cm.getTotalStats().getLeased(); // 当前正在使用的连接数
    int pending = cm.getTotalStats().getPending(); // 等待获取连接的线程数