限流算法
固定窗口
缺陷:最简单,但是不能精确限制,由于是计算的时间差,比如每10秒只能10个请求,8-10秒请求了10个,那么10-18秒就也无法请求了
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class MyFixedWindow {
private final int maxCount= 10; //限制次数
private final int windowUnit= 10 * 1000; // 单位时间 (毫秒)
private AtomicInteger nowCount ; //当前次数
private AtomicLong lastTime; // 最后一次请求的时间
public MyFixedWindow() {
this.nowCount=new AtomicInteger(1);
this.lastTime=new AtomicLong(0);
}
public synchronized boolean tryAcquire() {
System.out.println("-----"+nowCount.get());
long currentTime = System.currentTimeMillis(); //获取系统当前时间
if(currentTime-lastTime.get()>windowUnit){// 如果当前时间减去上次请求的时间大于目标设置的时间,就重置
nowCount.set(1);
lastTime.set(currentTime);
return true;
}else {
if(nowCount.get()>=maxCount){ //超过目标次数直接限流
return false;
}else{
nowCount.incrementAndGet(); //否则自增1
return true;
}
}
}
}
MyFixedWindow myFixedWindow= new MyFixedWindow();
/**
* 10 秒之内只能请求10次
*
* @return
*/
@GetMapping("/test")
public String test() {
if(myFixedWindow.tryAcquire()){
return "ok";
}else {
//执行降级
return "请稍后再重试";
// throw new RuntimeException();
}
}
可以看到限制没问题
滑动窗口
滑动窗口相对于固定窗口,把每个时间段分成一个格子,每次判断的都是当前盒子里面的请求次数,可以更加精准的实现接口的控制
import java.util.concurrent.atomic.AtomicInteger;
public class MyRollingWindow {
private final int maxCount= 10; //限制次数
private AtomicInteger nowRolling ; //当前窗口
private AtomicInteger nowCount ; //当前窗口已请求次数
public MyRollingWindow() {
this.nowRolling=new AtomicInteger(0);
this.nowCount=new AtomicInteger(1);
}
public synchronized boolean tryAcquire() {
System.out.println("-----"+nowRolling.get());
long currentTime = System.currentTimeMillis(); //获取系统当前时间
Integer now = Long.valueOf(currentTime / 1000 / maxCount).intValue();
if(nowRolling.get() != now ){// 如果算出的时间!=当前窗口
nowCount.set(1);
nowRolling.set(now);
return true;
}else {
if(nowCount.get()>=maxCount){ //超过目标次数直接限流
return false;
}else{
nowCount.incrementAndGet(); //否则自增1
return true;
}
}
}
}
MyRollingWindow myRollingWindow= new MyRollingWindow();
/**
* 10 秒之内只能请求10次
*
* @return
*/
@GetMapping("/test2")
public String test2() {
if(myRollingWindow.tryAcquire()){
return "ok";
}else {
//执行降级
throw new RuntimeException();
}
}
测试,同样没问题
漏桶算法
相当于上面两种限制的更加均衡,消耗的速度恒定,生成的速度可变
原理:计算本次与上次请求的时间差,然后限制这个时间段内的请求次数
缺点:如果一直以较高的频率请求,将会一直被拒绝
import java.util.concurrent.atomic.AtomicLong;
public class MyWaterLimit {
private AtomicLong waterLift ; //桶中剩余水容量
private final int leakRate = 1; //水流速率,(一秒一次)
private final int maxWater = 10; //桶大小
private AtomicLong lastTime; // 最后一次请求的时间
public MyWaterLimit() {
this.lastTime=new AtomicLong(0);
this.waterLift=new AtomicLong(0);
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
long l = (currentTime-lastTime.get()) / 1000 * leakRate;
long wf = waterLift.get() - l;//上次请求和本次请求时间间隔差,不能超过maxWater
waterLift.set(Math.max(0,wf));
lastTime.set(currentTime); // 放到这里更新时间,每次都会判断时间差是不是大于桶
if(waterLift.get()<maxWater){
// lastTime.set(currentTime); 如果只放到这里判断更新时间,会增加成功的概率,等于拒绝时,以第一次拒绝的时间开始计算,可以解决频繁请求一直被拒绝的问题
waterLift.incrementAndGet();
return true;
}else {
return false;
}
}
}
可以看到第一个限制10个请求成功是没问题的,第二次请求只有随机的几个,这是就由于计算的是 桶剩余的水-时间差,导致的相对均衡的限制
令牌桶
感觉相对比较完美,能根据时间差去计算还有多少令牌
生成的速度恒定,消耗的速度可变
import java.util.concurrent.atomic.AtomicLong;
public class MyTokenLimit {
private final int maxTokenCount = 10;//每次最多发10个令牌
private final int maxS= 10; //每间隔多久重新发一次令牌(单位秒)
private final long leakRate = 1; //发牌速度,每秒发几张
private AtomicLong lastTime; // 最后一次请求的时间
private AtomicLong nowToken ; //当前剩余令牌个数
public MyTokenLimit() {
lastTime = new AtomicLong(0);
nowToken = new AtomicLong(maxTokenCount);//默认给10个令牌
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
long sub = currentTime - lastTime.get() ; //时间差
if(sub >1000){ //如果大于1秒,重新计算还有多少牌
nowToken.set(Math.min(nowToken.get() + sub*leakRate/1000 ,maxTokenCount));// 剩余的牌+时间差内应该发的牌
}
lastTime.set(currentTime); //记录上一次成功的时候
if(nowToken.get()<1){//没有牌直接拒绝
return false;
}else { //有牌自减1
nowToken.decrementAndGet();//自减1
return true;
}
}
}
效果如图
Guava 限流
Guava可以很方便的实现限流,具体可以研究下参数,底层也是使用令牌桶实现的
private static final RateLimiter rateLimiter = RateLimiter.create(1);//qps 为1
@GetMapping("/test9")
public String test9() {
if (!rateLimiter.tryAcquire()) {
System.out.println("限流中......");
throw new RuntimeException();
}
System.out.println("请求成功");
return "ok";
}