HTTP客户端实现请求QPS的控制

背景:高德API调用QPS限制引发的问题

最近在项目中使用高德API进行地址转坐标时,频繁遇到CUQPS_HAS_EXCEEDED_THE_LIMIT错误。这是因为API对每秒请求量(QPS)有严格限制(如高德地图API默认QPS为3)。当客户端请求速度超过限制时,服务端会直接拒绝请求。为解决这一问题,我们需要在客户端实现QPS控制,确保请求速率符合服务端要求。

一、当前实现方案

ScheduledExecutorService + Semaphore

我们采用信号量(Semaphore)定时任务(ScheduledExecutorService)结合的方式控制QPS:

  1. 信号量:初始化为允许的最大并发数(如100)。

  2. 定时任务:每秒重置信号量许可数量,确保QPS不超过限制。

简单实现

public class QpsTaskCtrl {
  
    private final Semaphore semaphore;
    private final ScheduledExecutorService scheduler;

    public QpsTaskScheduler(int qps) {
        this.semaphore = new Semaphore(qps);

        this.scheduler = Executors.newScheduledThreadPool(1);

        this.scheduler.scheduleAtFixedRate(() -> {
            if( semaphore.availablePermits() >= qps ){
                semaphore.drainPermits();
            }
            semaphore.release();
        }, 0, 1000/qps, TimeUnit.MILLISECONDS);
    }

    public <T> T execute(Callable<T> callable){
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            LOGGER.error("Failed to acquire semaphore", e);
        }

        T result = null;
        try {
            result = callable.call();
        } catch (Exception e) {
            LOGGER.error("Failed to execute task", e);
            semaphore.release();
        } finally {

        }
        return result;
    }
}

测试

public static void main(String[] args) throws Exception {
        QpsTaskScheduler scheduler = new QpsTaskScheduler(3);
        for (int i = 0; i < 20; i++) {
            String testUrl = "https://jsonplaceholder.typicode.com/todos/"+ DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
            Request req = new Request.Builder().url(testUrl).build();
            Response res = scheduler.execute(()->{
                return new OkHttpClient().newCall(req).execute();
            });
            System.out.println( res );
        }
}

优点

  • 实现简单,无需引入外部依赖。

  • 适用于单机场景。

缺点

  • 定时任务可能因延迟导致信号量重置不及时。

  • 无法应对突发流量(如瞬时高并发)。

  • 分布式场景下需额外同步机制。

二、其他QPS控制方案及对比

除上述方案外,常见的QPS控制方法还包括:

1. 漏桶算法(Leaky Bucket)
  • 原理:请求进入“漏桶”,以固定速率流出。若桶满则拒绝请求。

  • 实现:使用队列存储请求,定时任务按QPS处理队列。

  • 优点:严格控制请求速率,平滑流量。

  • 缺点:无法利用突发流量(如短时间内允许更多请求)。

2. 令牌桶算法(Token Bucket)
  • 原理:每秒生成固定数量令牌,请求需消耗令牌。若令牌不足则等待或拒绝。

  • 实现:使用Guava RateLimiter(基于令牌桶)。

  • 优点:允许一定突发流量,灵活性高。

  • 缺点:实现复杂度较高。

代码示例

import com.google.common.util.concurrent.RateLimiter;

RateLimiter rateLimiter = RateLimiter.create(100); // QPS=100
rateLimiter.acquire(); // 阻塞直到有令牌可用
3. 滑动窗口计数器(Sliding Window Counter)
  • 原理:将时间窗口划分为多个子窗口,统计每个子窗口内的请求数。

  • 实现:使用环形数组记录每个子窗口的请求量。

  • 优点:精确控制QPS,避免固定窗口计数器的“突刺问题”。

  • 缺点:内存占用较高。

三、服务端接口限流

针对服务端的限流,我们需要通过控制客户端请求频率来控制,避免过于频繁的接口调用出错或封号等。

服务端限流一般有哪些方案呢?

1. Guava RateLimiter(单机)
  • 原理:基于令牌桶算法,线程安全。

  • 优点:简单易用,适用于单机服务。

  • 缺点:无法跨节点同步,分布式场景需配合Redis。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
    double qps(); // 每秒允许的请求数
    long warmupPeriod() default 0; // 预热期(毫秒)
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
@Aspect
@Component
public class RateLimiterAspect {
    // 使用ConcurrentHashMap存储不同接口的RateLimiter实例
    private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();

    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable {
        String methodKey = joinPoint.getSignature().getName(); // 以方法名作为限流标识
        RateLimiter limiter = rateLimiterMap.computeIfAbsent(
            methodKey, 
            k -> RateLimiter.create(
                rateLimiter.qps(),
                rateLimiter.warmupPeriod(),
                rateLimiter.timeUnit()
            )
        );

        if (!limiter.tryAcquire()) { // 尝试获取令牌,立即返回
            throw new TooManyRequestsException("接口请求过于频繁,请稍后再试");
        }
        return joinPoint.proceed();
    }
}
2. Redis计数器(分布式)
  • 原理:利用Redis的INCR命令统计请求数,结合EXPIRE实现时间窗口。

  • 实现
    -- lua脚本实现原子计数
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local current = tonumber(redis.call('GET', key) or "0")
    if current + 1 > limit then
        return 0
    else
        redis.call("INCR", key)
        redis.call("EXPIRE", key, 1)
        return 1
    end
    
  • 优点:支持分布式场景,精度高。

  • 缺点:依赖Redis性能,需考虑网络延迟。

3. Nginx限流(网关层)
  • 配置
    limit_req_zone $binary_remote_addr zone=one:10m rate=100r/s;
    server {
        location /api {
            limit_req zone=one burst=200;
            proxy_pass http://backend;
        }
    }
    
  • 优点:高效(内核级处理),不影响业务逻辑。

  • 缺点:配置较复杂,无法感知业务状态。

4. Sentinel(阿里开源框架)
  • 功能:基于滑动窗口限流,支持熔断、降级、负载保护。

  • 优点:功能全面,适用于微服务架构。

  • 缺点:引入额外依赖,学习成本较高。

结语
QPS控制是保障系统稳定性的重要环节。选择方案时需结合业务场景、技术成本和扩展性。通过客户端与服务端的双重限流,配合监控与报警,可有效避免因QPS超限导致的服务不可用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值