• 最近学习了限流相关的算法


    最近测试team在测试过程中反馈部分接口需要做一定的限流措施,刚好我也回顾了下限流相关的算法。常见限流相关的算法有四种:计数器算法, 滑动窗口算法, 漏桶算法, 令牌桶算法

    1.计数器算法(固定窗口)

     计数器算法是使用计数器在周期内累加访问次数,当达到设定的阈值时就会触发限流策略。下一个周期开始时,清零重新开始计数。此算法在单机和分布式环境下实现都非常简单,可以使用Redis的incr原子自增和线程安全即可以实现

     这个算法常用于QPS限流和统计访问总量,对于秒级以上周期来说会存在非常严重的问题,那就是临界问题,如下图:

     假设我们设置的限流策略时1分钟限制计数100,在第一个周期最后5秒和第二个周期的开始5秒,分别计数都是88,即在10秒时间内计数达到了176次,已经远远超过之前设置的阈值,由此可见,计数器算法(固定窗口)限流方式对于周期比较长的限流存在很大弊端。

     Java 实现计数器(固定窗口):

    package com.brian.limit;
    
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicInteger;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 固定窗口
     */
    @Slf4j
    public class FixWindow {
    
        private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        private final int limit = 100;
    
        private AtomicInteger currentCircleRequestCount = new AtomicInteger(0);
    
        private AtomicInteger timeCircle = new AtomicInteger(0);
    
        private void doFixWindow() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                log.info(" 当前时间窗口,第 {} 秒 ", timeCircle.get());
                if(timeCircle.get() >= 60) {
                    timeCircle.set(0);
                    currentCircleRequestCount.set(0);
                    log.info(" =====进入新的时间窗口===== ");
                }
                if(currentCircleRequestCount.get() > limit) {
                    log.info("触发限流策略,当前窗口累计请求数 : {}", currentCircleRequestCount);
                } else {
                    final int requestCount = (int) ((Math.random() * 5) + 1);
                    log.info("当前发出的 ==requestCount== : {}", requestCount);
                    currentCircleRequestCount.addAndGet(requestCount);
                }
               timeCircle.incrementAndGet();
            }, 0, 1, TimeUnit.SECONDS);
        }
    
        public static void main(String[] args) {
            new FixWindow().doFixWindow();
        }
        
    }

    2.滑动窗口算法

     滑动窗口算法是将时间周期拆分成N个小的时间周期,分别记录小周期里面的访问次数,并且根据时间的滑动删除过期的小周期。如下图,假设时间周期为1分钟,将1分钟再分为2个小周期,统计每个小周期的访问数量,则可以看到,第一个时间周期内,访问数量为92,第二个时间周期内,访问数量为104,超过100的访问则被限流掉了。

     由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。此算法可以很好的解决固定窗口算法的临界问题。

      Java实现滑动窗口:

    package com.brian.limit;
    
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicInteger;
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 滑动窗口
     * 
     * 60s限流100次请求
     */
    @Slf4j
    public class RollingWindow {
    
        private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        // 窗口跨度时间60s
        private int timeWindow = 60;
    
        // 限流100个请求
        private final int limit = 100;
    
        // 当前窗口请求数
        private AtomicInteger currentWindowRequestCount = new AtomicInteger(0);
    
        // 时间片段滚动次数
        private AtomicInteger timeCircle = new AtomicInteger(0);
    
        // 触发了限流策略后等待的时间
        private AtomicInteger waitTime = new AtomicInteger(0);
    
        // 在下一个窗口时,需要减去的请求数
        private int expiredRequest = 0;
    
        // 时间片段为5秒,每5秒统计下过去60秒的请求次数
        private final int slidingTime = 5;
    
        private ArrayBlockingQueue<Integer> slidingTimeValues = new ArrayBlockingQueue<>(11);
    
        public void rollingWindow() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
    
                if (waitTime.get() > 0) {
                    waitTime.compareAndExchange(waitTime.get(), waitTime.get() - slidingTime);
                    log.info("=====当前滑动窗口===== 限流等待下一个时间窗口倒计时: {}s", waitTime.get());
                    if (currentWindowRequestCount.get() > 0) {
                        currentWindowRequestCount.set(0);
                    }
                } else {
                    final int requestCount = (int) ((Math.random() * 10) + 7);
                    if (timeCircle.get() < 12) {
                        timeCircle.incrementAndGet();
                    }
                    
                log.info("当前时间片段5秒内的请求数: {} ", requestCount);
                currentWindowRequestCount.addAndGet(requestCount);
                log.info("=====当前滑动窗口===== {}s 内请求数: {} ", timeCircle.get()*slidingTime , currentWindowRequestCount.get());
    
                if(!slidingTimeValues.offer(requestCount)){
                    expiredRequest =  slidingTimeValues.poll();
                    slidingTimeValues.offer(requestCount);
                } 
    
                if(currentWindowRequestCount.get() > limit) {
                    // 触发限流
                    log.info("=====当前滑动窗口===== 请求数超过100, 触发限流,等待下一个时间窗口 ");
                    waitTime.set(timeWindow);
                    timeCircle.set(0);
                    slidingTimeValues.clear();
                } else {
                    // 没有触发限流,滑动下一个窗口需要,移除相应的:在下一个窗口时,需要减去的请求数
                    log.info("=====当前滑动窗口===== 请求数 <100, 未触发限流,当前窗口请求总数: {},即将过期的请求数:{}"
                            ,currentWindowRequestCount.get(), expiredRequest);
                    currentWindowRequestCount.compareAndExchange(currentWindowRequestCount.get(), currentWindowRequestCount.get() - expiredRequest);
                }
            }   
            }, 5, 5, TimeUnit.SECONDS);
        }
    
        public static void main(String[] args) {
            new RollingWindow().rollingWindow();
        }
        
    
    }

    计数器(固定窗口)和滑动窗口区别:

    计数器算法是最简单的算法,可以看成是滑动窗口的低精度实现。滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。也就是说,如果滑动窗口的精度越高,需要的存储空间就越大。

    3.漏桶算法

     漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。

     Java实现漏桶:

    package com.brian.limit;
    
    import java.util.concurrent.*;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 漏桶算法
     */
    @Slf4j
    public class LeakyBucket {
        private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        // 桶容量
        public  int capacity = 1000;
        
        // 当前桶中请求数
        public int curretRequest = 0;
    
        // 每秒恒定处理的请求数
        private final int handleRequest = 100;
    
        public void doLimit() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                final int requestCount = (int) ((Math.random() * 200) + 50);
                if(capacity > requestCount){
                    capacity -= requestCount;
                    log.info("<><>当前1秒内的请求数:{}, 桶的容量:{}", requestCount, capacity);
                    if(capacity <=0) {
                        log.info(" =====触发限流策略===== ");
                    } else {
                        capacity += handleRequest;
                        log.info("<><><><>当前1秒内处理请求数:{}, 桶的容量:{}", handleRequest, capacity);
                    }
                } else {
                    log.info("<><><><>当前请求数:{}, 桶的容量:{},丢弃的请求数:{}", requestCount, capacity,requestCount-capacity);
                    if(capacity <= requestCount) {
                        capacity = 0;
                    }
                    capacity += handleRequest;
                    log.info("<><><><>当前1秒内处理请求数:{}, 桶的容量:{}", handleRequest, capacity);
                }
            }, 0, 1, TimeUnit.SECONDS);
        }
    
        public static void main(String[] args) {
            new LeakyBucket().doLimit();
        }
    }

     漏桶算法有个缺点:如果桶的容量过大,突发请求时也会对后面请求的接口造成很大的压力。

    4.令牌桶算法

     令牌桶算法是程序以恒定的速度向令牌桶中增加令牌,令牌桶满了之后会丢弃新进入的令牌,当请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略。

     Java实现令牌桶:

    package com.brian.limit;
    
    import java.util.concurrent.*;
    
    import lombok.extern.slf4j.Slf4j;
    /**
     * 令牌桶算法
     */
    @Slf4j
    public class TokenBucket {
        private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
    
        // 桶容量
        public  int capacity = 1000;
        
        // 当前桶中请求数
        public int curretToken = 0;
    
        // 恒定的速率放入令牌
        private final int tokenCount = 200;
    
        public void doLimit() {
            scheduledExecutorService.scheduleWithFixedDelay(() -> {
                
                new Thread( () -> {
                    if(curretToken >= capacity) {
                        log.info(" =====桶中的令牌已经满了===== ");
                        curretToken = capacity;
                    } else {
                        if((curretToken+tokenCount) >= capacity){
                          log.info(" 当前桶中的令牌数:{},新进入的令牌将被丢弃的数: {}",curretToken,(curretToken+tokenCount-capacity));
                          curretToken = capacity;
                      } else {
                          curretToken += tokenCount;
                      }
                    }
                }).start();
    
                new Thread( () -> {
                    final int requestCount = (int) ((Math.random() * 200) + 50);
                    if(requestCount >= curretToken){
                        log.info(" 当前请求数:{},桶中令牌数: {},将被丢弃的请求数:{}",requestCount,curretToken,(requestCount - curretToken));
                        curretToken = 0;
                    } else {
                        log.info(" 当前请求数:{},桶中令牌数: {}",requestCount,curretToken);
                        curretToken -= requestCount;
                    }
                }).start();
            }, 0, 500, TimeUnit.MILLISECONDS);
        }
    
        public static void main(String[] args) {
            new TokenBucket().doLimit();
        }
        
    }

    漏桶算法和令牌桶算法区别:

    令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。而漏桶算法,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。
    总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证被别人的系统不被打垮,用漏桶算法

    参考博客:https://blog.csdn.net/weixin_41846320/article/details/95941361

         https://www.cnblogs.com/xuwc/p/9123078.html

  • 相关阅读:
    Java中IO流的总结
    Java常用集合体系以及相互区别
    TreeMap集合特点、排序原理
    HashMap集合
    TreeSet集合
    redis 数据类型详解 以及 redis适用场景场合
    You need tcl 8.5 or newer in order to run the Redis test
    PHP 获取二维数组中某个key的集合
    Linux 定时任务
    phpmailer邮件类
  • 原文地址:https://www.cnblogs.com/hlkawa/p/13111003.html
Copyright © 2020-2023  润新知