在工作中遇到一个问题,在访问第三方接口的时候有频控限制:20次/秒,所以当我的程序中并发量过高时就会在同一时间有上百次访问请求,为了解决这个频控问题,研究了以下两个解决方案。如果有遇到类似问题的朋友可以互相沟通
1. 令牌桶算法(Token Bucket)
令牌桶算法是一种常见的限流算法,它可以平滑地控制请求速率。其基本原理如下:
-
容量(Capacity):令牌桶的最大容量,表示桶中最多可以存放多少个令牌。
-
填充速率(Fill Rate):令牌桶以固定的速率向桶中添加令牌。
-
令牌(Tokens):当前桶中可用的令牌数量。
-
上次填充时间(Last Fill Time):记录上次填充令牌的时间。
工作原理:
-
填充令牌:定期检查当前时间与上次填充时间的时间差,计算这段时间内应该填充的令牌数量,并将其添加到桶中,但不超过桶的最大容量。
-
消费令牌:每次请求到来时,检查桶中是否有足够的令牌。如果有,则消耗相应数量的令牌并允许请求通过;如果没有,则拒绝请求。
优点:
-
可以应对突发流量,因为桶中可以存储一定数量的令牌。
-
可以平滑请求速率,避免流量突增。
示例代码解释:
public class TokenBucket {
private final int capacity;
private final double fillRate;
private double tokens;
private long lastFillTime;
private final Lock lock = new ReentrantLock();
public TokenBucket(int capacity, double fillRate) {
this.capacity = capacity;
this.fillRate = fillRate;
this.tokens = capacity;
this.lastFillTime = System.currentTimeMillis();
}
private void fill() {
lock.lock();
try {
long now = System.currentTimeMillis();
double tokensToAdd = (now - lastFillTime) * fillRate / 1000.0;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastFillTime = now;
} finally {
lock.unlock();
}
}
public boolean consume(int tokens) {
lock.lock();
try {
fill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true;
}
return false;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
TokenBucket tokenBucket = new TokenBucket(20, 20);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
if (tokenBucket.consume(1)) {
System.out.println("Request made");
} else {
System.out.println("Request throttled");
}
}).start();
}
}
}
2. 固定窗口计数器(Fixed Window Counter)
固定窗口计数器是一种简单的限流方法,它通过在固定时间窗口内计数来控制请求速率。
-
最大请求数(Max Requests):在固定时间窗口内允许的最大请求数。
-
窗口大小(Window Size):时间窗口的大小,通常以毫秒为单位。
-
请求计数(Requests):当前时间窗口内的请求计数。
-
上次重置时间(Last Reset Time):记录上次重置请求计数的时间。
工作原理:
-
重置计数器:定期检查当前时间与上次重置时间的时间差,如果超过了窗口大小,则重置请求计数和上次重置时间。
-
允许请求:每次请求到来时,检查当前时间窗口内的请求计数是否小于最大请求数。如果是,则允许请求通过并增加计数;否则,拒绝请求。
优点:
-
实现简单,易于理解。
-
适用于对请求速率要求不高的场景。
示例代码解释:
public class FixedWindowCounter {
private final int maxRequests;
private final long windowSize;
private int requests;
private long lastResetTime;
private final Lock lock = new ReentrantLock();
public FixedWindowCounter(int maxRequests, long windowSize) {
this.maxRequests = maxRequests;
this.windowSize = windowSize;
this.requests = 0;
this.lastResetTime = System.currentTimeMillis();
}
public boolean allowRequest() {
lock.lock();
try {
long now = System.currentTimeMillis();
if (now - lastResetTime > windowSize) {
requests = 0;
lastResetTime = now;
}
if (requests < maxRequests) {
requests++;
return true;
}
return false;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
FixedWindowCounter fixedWindowCounter = new FixedWindowCounter(20, 1000);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
if (fixedWindowCounter.allowRequest()) {
System.out.println("Request made");
} else {
System.out.println("Request throttled");
}
}).start();
}
}
}
3. 令牌桶算法-支持线程的挂起与唤醒(Token Bucket)
package com.neusoft.cps;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TokenBucket {
private final int capacity;
private final double fillRate;
private double tokens;
private long lastFillTime;
private final Lock lock = new ReentrantLock();
private final Condition tokensAvailableCondition = lock.newCondition();
public TokenBucket(int capacity, double fillRate) {
this.capacity = capacity;
this.fillRate = fillRate;
this.tokens = capacity;
this.lastFillTime = System.currentTimeMillis();
}
public void consume(int tokens, Runnable action) {
lock.lock();
try {
while (this.tokens < tokens) {
tokensAvailableCondition.await();
}
fill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
action.run();
} else {
throw new IllegalStateException("Unexpected state: not enough tokens after fill.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrupted while waiting for tokens", e);
} finally {
lock.unlock();
}
}
private void fill() {
long now = System.currentTimeMillis();
double tokensToAdd = (now - lastFillTime) * fillRate / 1000.0;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastFillTime = now;
if (tokensToAdd > 0) {
tokensAvailableCondition.signalAll();
}
}
public static void main(String[] args) {
TokenBucket tokenBucket = new TokenBucket(20, 20);
for (int i = 0; i < 100; i++) {
final int requestId = i;
new Thread(() -> {
tokenBucket.consume(1, () -> System.out.println("Request " + requestId + " made"));
}).start();
}
}
}
4. 使用Guava的RateLimiter
Guava库提供了一个方便的RateLimiter类,可以用来限制请求的速率。
import com.google.common.util.concurrent.RateLimiter;
public class RateLimiterExample {
private static final RateLimiter rateLimiter = RateLimiter.create(20.0); // 每秒20个请求
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
if (rateLimiter.tryAcquire()) {
// 处理请求
System.out.println("Request processed");
} else {
System.out.println("Request rejected");
}
// 或者使用rateLimiter.acquire() 会阻塞线程直到获取到许可证
// rateLimiter.acquire()
// 处理请求
// System.out.println("Request processed");
}).start();
}
}
}
5. 使用自定义的计数器
你可以使用一个简单的计数器来限制请求的速率。
import java.util.concurrent.atomic.AtomicInteger;
public class RequestLimiter {
private final int maxRequestsPerSecond;
private final AtomicInteger requestCount;
private long lastResetTime;
public RequestLimiter(int maxRequestsPerSecond) {
this.maxRequestsPerSecond = maxRequestsPerSecond;
this.requestCount = new AtomicInteger(0);
this.lastResetTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastResetTime > 1000) {
requestCount.set(0);
lastResetTime = currentTime;
}
if (requestCount.get() < maxRequestsPerSecond) {
requestCount.incrementAndGet();
return true;
} else {
return false;
}
}
public static void main(String[] args) {
RequestLimiter limiter = new RequestLimiter(20); // 每秒20个请求
for (int i = 0; i < 100; i++) {
new Thread(() -> {
if (limiter.tryAcquire()) {
// 处理请求
System.out.println("Request processed");
} else {
System.out.println("Request rejected");
}
}).start();
}
}
}