HTTP客戶端實(shí)現(xiàn)請(qǐng)求QPS的控制,你學(xué)會(huì)了嗎?
背景:高德API調(diào)用QPS限制引發(fā)的問(wèn)題
最近在項(xiàng)目中使用高德API進(jìn)行地址轉(zhuǎn)坐標(biāo)時(shí),頻繁遇到CUQPS_HAS_EXCEEDED_THE_LIMIT錯(cuò)誤。這是因?yàn)锳PI對(duì)每秒請(qǐng)求量(QPS)有嚴(yán)格限制(如高德地圖API默認(rèn)QPS為3)。當(dāng)客戶端請(qǐng)求速度超過(guò)限制時(shí),服務(wù)端會(huì)直接拒絕請(qǐng)求。為解決這一問(wèn)題,我們需要在客戶端實(shí)現(xiàn)QPS控制,確保請(qǐng)求速率符合服務(wù)端要求。
一、當(dāng)前實(shí)現(xiàn)方案
ScheduledExecutorService + Semaphore
我們采用信號(hào)量(Semaphore)與定時(shí)任務(wù)(ScheduledExecutorService)結(jié)合的方式控制QPS:
- 信號(hào)量:初始化為允許的最大并發(fā)數(shù)(如100)。
- 定時(shí)任務(wù):每秒重置信號(hào)量許可數(shù)量,確保QPS不超過(guò)限制。
簡(jiǎn)單實(shí)現(xiàn):
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;
}
}
測(cè)試
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 );
}
}
優(yōu)點(diǎn):
- 實(shí)現(xiàn)簡(jiǎn)單,無(wú)需引入外部依賴。
- 適用于單機(jī)場(chǎng)景。
缺點(diǎn):
- 定時(shí)任務(wù)可能因延遲導(dǎo)致信號(hào)量重置不及時(shí)。
- 無(wú)法應(yīng)對(duì)突發(fā)流量(如瞬時(shí)高并發(fā))。
- 分布式場(chǎng)景下需額外同步機(jī)制。
二、其他QPS控制方案及對(duì)比
除上述方案外,常見(jiàn)的QPS控制方法還包括:
1. 漏桶算法(Leaky Bucket)
- 原理:請(qǐng)求進(jìn)入“漏桶”,以固定速率流出。若桶滿則拒絕請(qǐng)求。
- 實(shí)現(xiàn):使用隊(duì)列存儲(chǔ)請(qǐng)求,定時(shí)任務(wù)按QPS處理隊(duì)列。
- 優(yōu)點(diǎn):嚴(yán)格控制請(qǐng)求速率,平滑流量。
- 缺點(diǎn):無(wú)法利用突發(fā)流量(如短時(shí)間內(nèi)允許更多請(qǐng)求)。
2. 令牌桶算法(Token Bucket)
- 原理:每秒生成固定數(shù)量令牌,請(qǐng)求需消耗令牌。若令牌不足則等待或拒絕。
- 實(shí)現(xiàn):使用Guava RateLimiter(基于令牌桶)。
- 優(yōu)點(diǎn):允許一定突發(fā)流量,靈活性高。
- 缺點(diǎn):實(shí)現(xiàn)復(fù)雜度較高。
代碼示例:
import com.google.common.util.concurrent.RateLimiter;
RateLimiter rateLimiter = RateLimiter.create(100); // QPS=100
rateLimiter.acquire(); // 阻塞直到有令牌可用
3. 滑動(dòng)窗口計(jì)數(shù)器(Sliding Window Counter)
- 原理:將時(shí)間窗口劃分為多個(gè)子窗口,統(tǒng)計(jì)每個(gè)子窗口內(nèi)的請(qǐng)求數(shù)。
- 實(shí)現(xiàn):使用環(huán)形數(shù)組記錄每個(gè)子窗口的請(qǐng)求量。
- 優(yōu)點(diǎn):精確控制QPS,避免固定窗口計(jì)數(shù)器的“突刺問(wèn)題”。
- 缺點(diǎn):內(nèi)存占用較高。
三、服務(wù)端接口限流
針對(duì)服務(wù)端的限流,我們需要通過(guò)控制客戶端請(qǐng)求頻率來(lái)控制,避免過(guò)于頻繁的接口調(diào)用出錯(cuò)或封號(hào)等。
服務(wù)端限流一般有哪些方案呢?
1. Guava RateLimiter(單機(jī))
- 原理:基于令牌桶算法,線程安全。
- 優(yōu)點(diǎn):簡(jiǎn)單易用,適用于單機(jī)服務(wù)。
- 缺點(diǎn):無(wú)法跨節(jié)點(diǎn)同步,分布式場(chǎng)景需配合Redis。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
double qps(); // 每秒允許的請(qǐng)求數(shù)
long warmupPeriod() default 0; // 預(yù)熱期(毫秒)
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
@Aspect
@Component
public class RateLimiterAspect {
// 使用ConcurrentHashMap存儲(chǔ)不同接口的RateLimiter實(shí)例
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(); // 以方法名作為限流標(biāo)識(shí)
RateLimiter limiter = rateLimiterMap.computeIfAbsent(
methodKey,
k -> RateLimiter.create(
rateLimiter.qps(),
rateLimiter.warmupPeriod(),
rateLimiter.timeUnit()
)
);
if (!limiter.tryAcquire()) { // 嘗試獲取令牌,立即返回
throw new TooManyRequestsException("接口請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試");
}
return joinPoint.proceed();
}
}
2. Redis計(jì)數(shù)器(分布式)
- 原理:利用Redis的INCR命令統(tǒng)計(jì)請(qǐng)求數(shù),結(jié)合EXPIRE實(shí)現(xiàn)時(shí)間窗口。
- 實(shí)現(xiàn):
-- lua腳本實(shí)現(xiàn)原子計(jì)數(shù)
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
- 優(yōu)點(diǎn):支持分布式場(chǎng)景,精度高。
- 缺點(diǎn):依賴Redis性能,需考慮網(wǎng)絡(luò)延遲。
3. Nginx限流(網(wǎng)關(guān)層)
- 配置:
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;
}
}
- 優(yōu)點(diǎn):高效(內(nèi)核級(jí)處理),不影響業(yè)務(wù)邏輯。
- 缺點(diǎn):配置較復(fù)雜,無(wú)法感知業(yè)務(wù)狀態(tài)。
4. Sentinel(阿里開(kāi)源框架)
- 功能:基于滑動(dòng)窗口限流,支持熔斷、降級(jí)、負(fù)載保護(hù)。
- 優(yōu)點(diǎn):功能全面,適用于微服務(wù)架構(gòu)。
- 缺點(diǎn):引入額外依賴,學(xué)習(xí)成本較高。
結(jié)語(yǔ)
QPS控制是保障系統(tǒng)穩(wěn)定性的重要環(huán)節(jié)。選擇方案時(shí)需結(jié)合業(yè)務(wù)場(chǎng)景、技術(shù)成本和擴(kuò)展性。通過(guò)客戶端與服務(wù)端的雙重限流,配合監(jiān)控與報(bào)警,可有效避免因QPS超限導(dǎo)致的服務(wù)不可用。