什么是限流算法
限流是指在高并发、大流量请求的情况下,限制新的流量对系统的访问,从而保证系统服务的安全性。
应用场景
请求过多的发生原因:
- 热点业务带来的突发请求
- 调用方bug导致的突发请求
- 恶意攻击请求
一般来说,秒杀系统就有这种场景,当秒杀开启的时候,由于频繁的请求访问,导致服务器压力过大,有可能会导致崩溃
- 因此可以做窗口限流算法,限制每个用户在一定时间内只能够访问 N 次,当超过访问的次数,临时加入黑名单,设置一个过期时间,可以与 Redis 结合实现
限流算法原理
固定窗口限流算法
固定的窗口:也就是时间区间内消息是固定的,也就导致了不能去预防突发的流量
超出窗口的请求会被丢弃
代码实现:
/**
* 固定窗口限流算法
* 阈值为 3
* 时间窗口为 1 s
*/
static final int THRESHOLD = 3;
static final long window = 1000;
static long startTime = System.currentTimeMillis();
static int count = 0;
static boolean fixedWindow(){
// 当前时间
long curTime = System.currentTimeMillis();
// 判断当前时间是否还在区间内
if(curTime - window > startTime){
// 更新开始时间
startTime = curTime;
// 重置计数
count = 0;
}
// 判断请求是否达到阈值
if(count < THRESHOLD){
count++;
return true;
}
// 请求已经达到阈值
return false;
}
/**
* 简单测试
*/
public static void main(String[] args) throws InterruptedException {
// 模拟 1秒 内无限请求
long startTime = System.currentTimeMillis();
int index = 0;
while(true){
long endTime = System.currentTimeMillis();
// 如果提前跳出
if(endTime - startTime >= 900){
break;
}
Thread.sleep(50);
System.out.println( index++ + (fixedWindow()? "允许请求" : "拒绝请求"));
}
// 1 秒后再请求
Thread.sleep(100);
System.out.println("一秒后" + (fixedWindow()? "允许请求" : "拒绝请求"));
}
测试结果:
0允许请求
1允许请求
2允许请求
3拒绝请求
4拒绝请求
5拒绝请求
6拒绝请求
7拒绝请求
8拒绝请求
9拒绝请求
10拒绝请求
11拒绝请求
12拒绝请求
13拒绝请求
14拒绝请求
一秒后允许请求
突发流量的情况:也是临界问题,就是当在1秒
的前后,都发送了3条消息,很明显这样会违反了
滑动窗口限流算法
滑动窗口算法和固定窗口的算法差不多,但是由全部更新
变成了部分更新
,一次只更新一个窗口,但是这样的问题就是当达到限流后,请求都会被暴力拒绝,且窗口滑动的距离是比较缓慢的
更加的灵活,能够应对 突发流量 的问题
- 下面的图示是每 0.5 秒更新一个窗口,很明显,
1 ~ 1.5
这个时间段内请求3个不会超过阈值,但如果是并发流量1.9 ~ 2
刷新前后都无限请求,在下图所示的情况,只会接收多一个请求,略微超出阈值,通过更细致的设置,可以使这个范围更小。 - 相比于固定窗口,滑动窗口对突发流量的问题很明显的缓解了压力
图示:
实现代码:
/**
* 滑动窗口限流算法
* 窗口初始为 10
* 时间窗口为 0.5 s
*/
// 窗口大小
static int size = 10;
// 窗口 大小:10
static boolean[] window = new boolean[size];
// 窗口指针索引
static int index = 0;
// 上一个开始时间
static long startTime = System.currentTimeMillis();
// 时间区间 1s
static long time = 1000;
// 当前窗口计数总和
static long counter = 0;
static boolean slideWindow(){
// 当前时间
long curTime = System.currentTimeMillis();
// 更新窗口(这个更新窗口应该是异步更新才对)
if(curTime - startTime > time){
// 更新开始时间
startTime = curTime;
// 更新窗口为可使用
window[index] = false;
counter--;
}
// 判断当前窗口是否可用
if(!window[index]){
// 记录窗口已被使用
window[index] = true;
// 移动到下一个节点 (超出大小求余)
index++;
index = index % size;
// 计数
counter++;
return true;
}
// 请求已经达到阈值
return false;
}
/**
* 简单测试
*/
public static void main(String[] args) throws InterruptedException {
// 模拟 1秒 内无限请求
long startTime = System.currentTimeMillis();
int index = 0;
while(true){
long endTime = System.currentTimeMillis();
// 提前跳出,以防最后一个方法调用的时候实际计时已经超过 1秒
if(endTime - startTime >= 900){
break;
}
// 睡眠 50ms
Thread.sleep(50);
System.out.println( index++ + (slideWindow()? "允许请求" : "拒绝请求"));
}
// 1 秒后再请求
Thread.sleep(100);
System.out.println("一秒后" + (slideWindow()? "允许请求" : "拒绝请求"));
}
测试:
0允许请求
1允许请求
2允许请求
3允许请求
4允许请求
5允许请求
6允许请求
7允许请求
8允许请求
9允许请求
10拒绝请求
11拒绝请求
12拒绝请求
13拒绝请求
14拒绝请求
一秒后允许请求
漏桶限流算法
桶漏算法与上两个算法不同,它是要求平滑的处理,保证整体的一个速率
原理:就是往桶里注水,这个注水的速率可以是任意
的,如果超过桶的容量就需要被丢弃。因为桶的容量是不变
的,处理速率也是固定
的,所以保证了整体的速率
- 和上两个对比来说,就是窗口我可能不使用,速率可以是零,但是桶的水滴一定会漏,所以说桶漏是恒定的速率
对于突发流量问题也是能够解决的,因为桶的大小是固定的,与时间内突发无关,即使请求增多,处理速率也没有什么变化
图示:
实现代码:
/**
* 桶漏限流算法
*/
// 桶的容量
static long capacity = 10;
// 当前水量
static long currWater = 0;
// 上次的结束时间(初始化)
static long startTime = System.currentTimeMillis();
// 出水速率 (1 滴/s)
static long outputRate = 1;
static boolean bucketLimit(){
// 获取当前时间
long curTime = System.currentTimeMillis();
// 更新出水量
if(curTime - startTime > 1000){
// 计算出水量 = (结束时间 - 开始时间) / 1000 * 出水速率
System.out.println(curTime - startTime);
long outputWater = (curTime - startTime) / 1000 * outputRate;
// 计算剩余水量 = 当前水量 - 出水量
currWater = Math.max(0, currWater - outputWater);
// 更新时间
startTime = curTime;
}
// 判断桶是否满了
if(currWater < capacity){
// 未满,请求通过,当前水量 + 1
currWater++;
return true;
}
// 桶满,拒绝请求
return false;
}
public static void main(String[] args) throws InterruptedException {
// 模拟 1秒 内无限请求
long startTime = System.currentTimeMillis();
int index = 0;
while(true){
long endTime = System.currentTimeMillis();
// 如果提前跳出
if(endTime - startTime >= 900){
break;
}
Thread.sleep(50);
System.out.println( index++ + (bucketLimit()? "允许请求" : "拒绝请求"));
}
// 1 秒后再请求
Thread.sleep(100);
System.out.println("一秒后" + (bucketLimit()? "允许请求" : "拒绝请求"));
}
测试结果:
0允许请求
1允许请求
2允许请求
3允许请求
4允许请求
5允许请求
6允许请求
7允许请求
8允许请求
9允许请求
10拒绝请求
11拒绝请求
12拒绝请求
13拒绝请求
14拒绝请求
15拒绝请求
1047
一秒后允许请求
令牌桶限流算法
令牌桶算法和桶漏算法的主角都是 桶
,不过桶里装的是令牌
,和桶漏是相反的,这边是颁发令牌
- 以恒定的速率产生令牌
- 处理速度是任意的
令牌桶能够储存一定的令牌数,同样当超过这个数量的时候,令牌也需要丢弃,当请求到来的时候,需要判断是是能否从桶里获得一个令牌,如果能获得,说明可以访问。
对于突发流量问题也是能够解决的,突发流量过来的时候,也只能根据令牌桶的数量去处理请求,多出的请求,没有相应的令牌也无法被处理
图示:
实现代码:
/**
* 令牌桶限流算法
*/
// 令牌桶的容量
static long capacity = 10;
// 当前令牌数
static long tokens = 10;
// 上次的结束时间(初始化)
static long startTime = System.currentTimeMillis();
// 产生令牌速率 (1 个/s)
static long inputRate = 1;
static boolean tokenBucketLimit(){
// 获取当前时间
long curTime = System.currentTimeMillis();
// 更新令牌数
if(curTime - startTime > 1000){
// 计算出水量 = (结束时间 - 开始时间) / 1000 * 出水速率
System.out.println(curTime - startTime);
long token = (curTime - startTime) / 1000 * inputRate;
// 计算剩余水量 = 当前水量 - 出水量
tokens = Math.min(capacity, tokens + token);
// 更新时间
startTime = curTime;
}
// 判断令牌桶中是否有令牌
if(tokens > 0){
// 颁发令牌,请求通过
tokens--;
return true;
}
// 没有令牌,拒绝请求
return false;
}
/**
* 简单测试
*/
public static void main(String[] args) throws InterruptedException {
// 模拟 1秒 内无限请求
long startTime = System.currentTimeMillis();
int index = 0;
while(true){
long endTime = System.currentTimeMillis();
// 如果提前跳出
if(endTime - startTime >= 900){
break;
}
Thread.sleep(50);
System.out.println( index++ + (tokenBucketLimit()? "允许请求" : "拒绝请求"));
}
// 1 秒后再请求
Thread.sleep(100);
System.out.println("一秒后" + (tokenBucketLimit()? "允许请求" : "拒绝请求"));
}
测试结果:
0允许请求
1允许请求
2允许请求
3允许请求
4允许请求
5允许请求
6允许请求
7允许请求
8允许请求
9允许请求
10拒绝请求
11拒绝请求
12拒绝请求
13拒绝请求
14拒绝请求
15拒绝请求
1018
一秒后允许请求