计数器限流算法
限制在某个时间段内处理请求的次数,比如 1 分钟内只能处理 100 次请求
第一分钟的最后 10 秒和第二分种的开始 10 秒,它们都在各自 1 分钟周期内同时满足访问 100 次,那么它们在这个 20 秒的时间段就访问了 200 次超过了限流负荷,服务可能就挂了
static final int limit = 100;
// 1 秒种内只能处理 100 个请求
public boolean access(String key) {
int val = jedis.incr(key);
if (val == 1) {
jedis.pexpire(key, 1000);
return true;
}
return val <= limit;
}
漏桶限流算法
漏桶以一定的速率向外漏水,服务器就以这个速率来处理请求,通过这种方式就能避免服务器承载请求过量,当遭大规模遇突发请求的时候,进入率大于出水率,漏桶水位会不断上升,当水位溢出的时候触发拒绝策略
它的缺点是无法处理突发的大量的并发请求,服务器只能以漏水的恒定速率去处理请求,如果漏水速率大于等于进水速率那么,漏桶永远都不会满
Redis-cell 提供了漏桶限流的实现,通过再项目中引入 Redis-cell 可以实现单机或者分布式限流
// 用户回复接口,每 60 秒只能只能调用 30 次,漏洞容量为 100
> cl.throttle user:reply 100 30 60
令牌桶限流算法
以一定的速率往桶中放入令牌,桶满了后丢弃令牌
服务器处理客户端请求的时候先从桶中获取令牌,获取成功处理请求,获取失败则采用拒绝策略处理请求
采用 Guava 来实现令牌桶算法
// 每秒生成 50 个令牌
final static RateLimiter rateLimiter = RateLimiter.create(50, 1, TimeUnit.SECONDS);
public boolean access() {
boolean acquired = rateLimiter.tryAcquire();
if (!acquired) {
// 令牌桶空了,被限流了
return false;
}
return true;
}
滑动窗口限流算法
我们通过窗口间隔时间定时清空上一个窗口的请求数量,然后切换到下一个窗口,通过取余的方式循环利用一个数组来实现
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
public class SlidingWindowLimiter {
// 默认美秒,允许通过请求的总数
private int limitRequestTotal = 60;
// 窗口数量
private int windowNum = 5;
// 窗口之间的时间间隔 1000MS / 10 窗口,一个窗口 100MS
private int interval = 1000 / windowNum;
// 每个窗口能够通过的请求数量
private int windowRequestLimit = limitRequestTotal / windowNum;
// 多少个窗口
private Window[] windows = new Window[windowNum];
// 当前窗口
private int currentIndex = 0;
// 当前总的滑动窗口请求总数
private AtomicInteger currentRequestTotal = new AtomicInteger(0);
// 默认参数每分钟允许请求 60 次,5 个窗口,每个窗口 200MS 允许请求,每个窗口允许请求上限 20 次
public SlidingWindowLimiter() {
}
public SlidingWindowLimiter(int secondPer, int windowNum, int limitRequestTotal) {
this.windowNum = windowNum;
this.interval = (secondPer * 1000) / windowNum;
this.limitRequestTotal = limitRequestTotal;
this.windowRequestLimit = limitRequestTotal / windowNum;
for (int i = 0; i < windows.length; i++) {
windows[i] = new Window();
}
Timer timer = new Timer();
// 每隔一个窗口时间,清空上一个窗口积累的请求数
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
// 下一个窗口
currentIndex = (currentIndex + 1) % windowNum;
// 上一个窗口的请求总数
int beforeCount = windows[currentIndex].requestCount.get();
currentRequestTotal.addAndGet(-beforeCount);
}
};
timer.schedule(timerTask, interval, interval);
}
public boolean doLimit() {
if (currentRequestTotal.get() > limitRequestTotal) {
return false;
}
if (windows[currentIndex].requestCount.incrementAndGet() > windowRequestLimit) {
return false;
}
// 请求总数加 1
currentRequestTotal.incrementAndGet();
return true;
}
class Window {
private AtomicInteger requestCount;
public Window() {
this.requestCount = new AtomicInteger(0);
}
}
}
测试代码
@Test
public void testSlidingWindowLimiter() throws InterruptedException {
// 每秒允许发起 100 次请求,5 个窗口,每个窗口 200 MS,每个窗口允许访问 20 次
SlidingWindowLimiter limiter = new SlidingWindowLimiter(1,5,100);
for (int i = 0; i < 100; i++) {
boolean access = limiter.doLimit();
if (access) {
System.out.println(i +"访问成功");
} else {
System.out.println(i+ "被限流了");
}
Thread.sleep(5);
}
}
从输出结果来看,当 i = 19 的时候,表示发起了 20 个请求都是成功的,当 i = 20 的时候,sleep 了 100MS + 代码处理时间也就是 100 多 MS(小于 200),所以后面几十毫秒处理都会失败,然后到窗口切换时间后,清空上一个窗口请求数量,下一个窗口就能继续处理请求
可以看出滑动窗口限流算法,跟窗口数量关系密切,窗口数量越多,窗口间隔时间越短,就能够更加及时的处理新到来的请求,统计限流就能更加精确