记录今天研究的两种限流算法,令牌桶算法和漏桶算法,他们是两种非常常用的限流算法,比如Guava的RateLimiter限流组件,它是基于令牌桶算法实现的
令牌桶算法
设计思想
有一个装令牌的桶,我以恒定的速率往桶里加入令牌,当请求来时从桶里获取令牌,若成功获取令牌则放行,若桶内没有可获取的令牌则获取失败被限流。
实现思路
可以开启一个线程定时往桶里加令牌,但是有另一种方式更轻量级:记录上次令牌刷新的时间oldtime,当请求来时计算当前时间和oldtime的时间差*速率就可以这段时间产生的令牌数量,这种实现方式也很常见,比如在redis中判断key是否过期也是采用这种方式
- 计算当前时间和上次令牌刷新的时间oldtime的时间差*速率就可以这段时间产生的令牌数量,加上之前剩余的令牌得到当前桶内的令牌
- 如果令牌大于0则放行本次请求,并消耗本次令牌,如果令牌等于0则本次请求被限流
代码:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
public class TokenBucket {
private final int capacity; // 令牌桶容量
private final int rate; // 令牌生成速率(每秒生成的令牌数)
private int tokens; // 当前令牌数量
private long lastRefillTime; // 上次令牌生成时间
public TokenBucket(int capacity, int rate) {
this.capacity = capacity;
this.rate = rate;
this.tokens = 0;
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean allowRequest() {
refillTokens();
// 如果令牌数量小于1,则拒绝请求
if (tokens < 1) {
return false;
}
// 令牌数量减1
tokens--;
return true;
}
private void refillTokens() {
long currentTime = System.currentTimeMillis();
long elapsedTime = currentTime - lastRefillTime;
int tokensToAdd = (int) (elapsedTime / 1000 * rate);
// 令牌数量不能超过容量
tokens = Math.min(tokens + tokensToAdd, capacity);
lastRefillTime = currentTime;
}
public static void main(String[] args) throws InterruptedException {
TokenBucket tokenBucket = new TokenBucket(10, 2); // 令牌桶容量为10,每秒生成2个令牌
// 模拟请求 创建一个最大线程数5个线程的线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,0, TimeUnit.MINUTES,new ArrayBlockingQueue<Runnable>(10));
// 每秒有5个请求,连续5轮,看最终有多少能通过
for (int k = 0; k <5 ; k++) {
Thread.sleep(1000);
for (int i = 0; i < 5; i++) {
int j =i;
poolExecutor.execute(()->{
if (tokenBucket.allowRequest()) {
log.info("Request " + (j + 1) + ": Allowed");
}else
log.info("Request " + (j + 1) + ": Rejected");
});
}
}
}
}
结果
第一轮:
21:54:18.031 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Rejected
21:54:18.031 [pool-1-thread-3] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Allowed
21:54:18.031 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
21:54:18.031 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:18.031 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
第二轮
21:54:19.030 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Allowed
21:54:19.031 [pool-1-thread-3] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Rejected
21:54:19.031 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
21:54:19.030 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:19.031 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
第三轮
21:54:20.036 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:20.036 [pool-1-thread-3] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Allowed
21:54:20.037 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
21:54:20.036 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Rejected
21:54:20.037 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
第四轮
21:54:21.040 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
21:54:21.040 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Rejected
21:54:21.040 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:21.040 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
21:54:21.040 [pool-1-thread-3] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Allowed
第五轮
21:54:22.046 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Rejected
21:54:22.046 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
21:54:22.046 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:22.046 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
21:54:22.046 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Allowed
特点:
- 4个重要组成要素:容量、速率、时间、令牌数量
- 设计思想:通过控制生成令牌的速率间接控制流量,他希望控制的使一段时间内的平均流量,允许某些时刻出现大流量的情况
- 由于令牌可以堆积,所以在令牌桶内令牌充足时突然有大量的请求打来也可以得到处理
漏桶算法
设计思想
相当于是一个漏斗,当请求到来时将请求装入漏斗,通过控制出口的速率来控制进入系统的流量。
注意:
他的设计思想是在任意时刻都只允许出口速率大小的流量进入系统,不允许任意时刻超过出口流量
现在网上很多漏斗算法的实现都违背了漏斗算法的设计思想,包括从chatgpt得到的答案也是如此,最后得到的漏斗算法实现其实和令牌桶算法没有任何区别
比如举个错误的例子:
/**
* 每秒处理数(出水率)
*/
private long rate;
/**
* 当前剩余水量
*/
private long currentWater;
/**
* 最后刷新时间
*/
private long refreshTime;
/**
* 桶容量
*/
private long capacity;
/**
* 漏桶算法
* @return
*/
boolean leakybucketLimitTryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
refreshTime = currentTime; // 刷新时间
// 当前剩余水量还是小于桶的容量,则请求放行 ERROR、ERROR、ERROR
if (currentWater < capacity) {
currentWater++;
return true;
}
// 当前剩余水量大于等于桶的容量,限流
return false;
}
上述算法在请求进来时判断当前桶是否装满,如果没装满则放行,实际上是错的很离谱
我们举个例子,当我们有一个容量为10的桶,希望控制速率为2QPS,现在桶是空的,有10个请求进来
上述算法会将10个请求都放行,也就是说当前时刻的QPS达到了10QPS远远大于我们期望的2QPS(这样的实现其实就是另一种令牌桶算法了)
漏桶算法则只会将前两个请求放行,剩余8个请求留在桶中等待下一次放行时刻到来则再放行2个请求,从而达到任意时刻都不超过2QPS
对比
令牌桶算法是希望控制一段时间内流入系统的平均速率,允许某些时刻出现不超过最大容量的大流量出现
漏桶算法是希望控制任意时刻流入系统的流量都不要超过预定速率
很多人说令牌桶算法允许某些时刻出现大流量的特点是其优点,但是我觉得两种算法都有其应用场景,应该根据具体的业务需求去选择何种算法