限流算法

固定窗口

缺陷:最简单,但是不能精确限制,由于是计算的时间差,比如每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";
    }

posted @ 2023-05-31 17:43  猥琐熊花子酱  阅读(37)  评论(0编辑  收藏  举报