限流算法原理和实现

什么是限流算法

限流是指在高并发、大流量请求的情况下,限制新的流量对系统的访问,从而保证系统服务的安全性。

应用场景

请求过多的发生原因:

  • 热点业务带来的突发请求
  • 调用方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
一秒后允许请求

猜你喜欢

转载自blog.csdn.net/DespairC/article/details/126765665