盘点微服务经典限流算法,附Java演示源码

208 阅读2分钟

写在前面

遥想当年,面对P8,意气风发,问我意向,开口微服务,闭眼高并发,上到集群,下到秒杀
 
P8一乐:来个基础,说说限流算法?
 
me   :wow ......啊吧啊吧
 

固定窗口

概括:10秒内,只允许请求5次

维护一个固定大小的时间窗口,如 (00:00,00:10) 这个窗口只允许5次流量
                         (00:10,00:20) 这个窗口只允许5次流量
                         
依次记录每个窗口请求的req次数,超出阈值时,拒绝请求     
public class LimitMethodOne {

    // 上一次请求时间,即左窗口
    static long lastReqTime = System.currentTimeMillis();

    // 计数器 记录当前请求了多少流量,流量通过则+1
    static int counter = 0;

    // 阈值
    static int threshold = 3;

    // 窗口大小为10秒
    static long windowUnit = 10 * 1000l;

    static synchronized boolean fixedWindowsTryAcquire() {
        // 当前请求时间,即右窗口
        long currentTime = System.currentTimeMillis();
        // 判断是否处于限制时间窗口内
        // 超出了时间窗口,则进入下一个窗口,计数器归0
        if (currentTime - lastReqTime > windowUnit) {
            //计数器归零
            counter = 0;
            // 下一个窗口 左值
            lastReqTime = currentTime;
        }
        //判断此请求 窗口内,即10s内,是否超出了阈值,未超出,表示可以访问
        if (counter < threshold) {
            //计数器+1
            counter++;
            return true;
        }
        return false;
    }


    public static void main(String[] args) throws Exception {
        // 模拟线程并发,同时20个线程启动,并发访问
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        List<Callable<String>> runnables = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            runnables.add(new Callable() {
                @Override
                public String call() throws Exception {
                    // 表示此段时间内,流量是稳定的
                    if (fixedWindowsTryAcquire()) {
                        System.out.println("流量安全:" + Thread.currentThread());
                        System.out.println();
                    } else {
                        System.out.println("流量不安全:" + Thread.currentThread());
                    }
                    return "1";
                }
            });
        }
        executorService.invokeAll(runnables);
        System.in.close();
    }
}
缺陷: (00:00,00:10)  如果(00:08,00:10) 发生了5次req请求
      (00:10,00:20)  如果(00:10,00:12) 发生了5次req请求
      
那么对于窗口(00:08,00:18) 10秒内发生了10次req请求,也就是临界问题,则不符合规则10秒内5次req请求
也就是滑动窗口解决

滑动窗口

 滑动窗口的思想背景:可见基本Leetcode算法

leetCode滑动窗口

 概括:10秒内,只允许请求5次
 固定窗口的切分是 (00:00,00:10),(00:10,00:20),(00:20,00:30)
 那么滑动窗口的思想则是更加细微的切分,也就是将大窗口,逐渐切分成小窗口,例如以1秒的格式递进
 (00:00,00:10)
 (00:01,00:11)
 (00:02,00:12)
 (00:03,00:13)
public class LimitMethodTwo {

    // 计数器,用于记录每个窗口的 技术,key为开始计数时间
    private static Map<Long, Integer> counters = new HashMap<>();

    //单位划分的小周期
    static private int SUB_CYCLE = 1;

    // 每10秒的阈值
    static private int threshold = 5;

    // 窗口为10秒
    static Long windowUnit = 10 * 1000l;

    synchronized static boolean slidingWindowsTryAcquire() {
        // 当前时间
        Long currentTime = System.currentTimeMillis() / 1000;
        //当前窗口总请求数
        int currentWindowNum = countCurrentWindow(currentTime);
        //超过阈值
        if (currentWindowNum > threshold) {
            return false;
        }
        // 计数器+1
        counters.put(currentTime, (Objects.isNull(counters.get(currentTime)) ? 0 : counters.get(currentTime)) + 1);
        return true;
    }

    /**
     * 统计当前窗口的请求数
     */
    static int countCurrentWindow(Long currentWindowTime) {
        //滑动计算窗口开始位置
        Long startTime = currentWindowTime - windowUnit;
        int count = 0;
        //遍历存储的计数器
        Iterator iterator = counters.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Long, Integer> entry = (Map.Entry<Long, Integer>) iterator.next();
            // 删除无效过期的子窗口计数器
            if (entry.getKey() < startTime) {
                iterator.remove();
            } else {
                //累加当前窗口的所有计数器之和
                count = count + entry.getValue();
            }
        }
        return count;
    }


    public static void main(String[] args) throws Exception {
        // 模拟线程并发,同时20个线程启动,并发访问
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        List<Callable<String>> runnables = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            runnables.add(new Callable() {
                @Override
                public String call() throws Exception {
                    // 表示此段时间内,流量是稳定的
                    if (slidingWindowsTryAcquire()) {
                        System.out.println("流量安全:" + Thread.currentThread());
                        System.out.println();
                    } else {
                        System.out.println("流量不安全:" + Thread.currentThread());
                    }
                    return "1";
                }
            });
        }
        executorService.invokeAll(runnables);
        System.in.close();
    }
}
缺陷:滑动窗口算法虽然解决了固定窗口的临界问题,但是一旦到达限流后,请求都会直接暴力被拒绝,也就是会丟是部分请求

漏桶算法

 概括:假设你有一个容量为10l的桶,下面有个洞,这个桶以1l/s的恒定速度出水,只要桶未满,即可入桶   
public class LimitMethodThree {

    //出水率 恒定速率出水
    static long rate = 1l;

    // 上一次请求刷新时间
    static long refreshTime = System.currentTimeMillis();

    static long currentWater = 0;

    // 桶最大容量
    static long capacity = 5l;

    /**
     * 漏桶算法
     *
     * @return
     */
    static synchronized boolean leakybucketLimitTryAcquire() {
        // 当前时间
        long currentTime = System.currentTimeMillis();
        // 出水量
        long outWater = (currentTime - refreshTime) / 1000 * rate;
        // 当前水剩余量
        currentWater = Math.max(0, currentWater - outWater);
        //当前水余量小于最大容量 桶未满 流量进入
        if (currentWater < capacity) {
            //容量+1
            currentWater++;
            return true;
        }
        // 限流
        return false;
    }

    public static void main(String[] args) throws Exception {
        // 模拟线程并发,同时20个线程启动,并发访问
        ExecutorService executorService = Executors.newFixedThreadPool(200);
        List<Callable<String>> runnables = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            runnables.add(new Callable() {
                @Override
                public String call() throws Exception {
                    // 表示此段时间内,流量是稳定的
                    if (leakybucketLimitTryAcquire()) {
                        System.out.println("流量安全:" + Thread.currentThread());
                        System.out.println();
                    } else {
                        System.out.println("流量不安全:" + Thread.currentThread());
                    }
                    return "1";
                }
            });
        }
        executorService.invokeAll(runnables);
        System.in.close();
    }
}
缺陷: 天突下暴雨⛈️,桶迅速装满,下面洞流出率水小, 无用
     也就是突发流量下,无法应对
     
     如何应对?见令牌桶
     

令牌桶算法

  概括:楼主买劳斯莱斯的故事
  
      楼主想买劳斯莱斯(req请求),前期是必须要有购车资格卡(令牌)
      
      杭州政府每个月30天,放出共500张购车资格卡(令牌发放速率),在官网(桶)上
      
      楼主抢到了购车资格卡(令牌),则可以买劳斯莱斯(允许请求)
      
      未抢到,无购车资格(拒绝请求)

image.png

public class LimitMethodFour {

    // 令牌的产生速率
    static long rate = 10l;

    // 刷新时间
    static long refreshTime = System.currentTimeMillis();

    //当前剩余令牌数
    static long currentCount = 0;

    // 令牌桶最大容量
    static long capacity = 10;

    synchronized static boolean tokenBucketTryAcquire() {
        // 当前时间
        long currentTime = System.currentTimeMillis();
        // 这段时间产生的令牌
        long generationCount = ((currentTime - refreshTime) / 1000) * rate;
        // 当前剩余令牌
        currentCount = Math.min(capacity, generationCount + currentCount);
        // 更新时间
        refreshTime = currentTime;
        // 如果当前令牌大于0 申请到令牌,流量通过
        if (currentCount > 0) {
            // 令牌数-1
            currentCount--;
            return true;
        }
        // 没有令牌
        return false;
    }

    public static void main(String[] args) throws Exception {
        // 模拟线程并发,同时20个线程启动,并发访问
        ExecutorService executorService = Executors.newFixedThreadPool(200);
        List<Callable<String>> runnables = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            runnables.add(new Callable() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(1000l);
                    // 表示此段时间内,流量是稳定的
                    if (tokenBucketTryAcquire()) {
                        System.out.println("流量安全:" + Thread.currentThread());
                        System.out.println();
                    } else {
                        System.out.println("流量不安全:" + Thread.currentThread());
                    }
                    return "1";
                }
            });
        }
        executorService.invokeAll(runnables);
        System.in.close();
    }
}
思考:对比于漏桶算法的缺陷,突降大雨,桶爆的缺陷

    令牌桶则可以通过增大令牌发送速率来解决 (如杭州每个月发30000000000000张购车资格卡)
    

参考资料

面试必备:4种经典限流算法讲解