• 常见的几种限流算法代码实现(JAVA)


    最近在学习Sentinel组件需要了解限流算法相关的知识,正好在微信公众号上看到了一篇不错的文章,在此记录一下以下是原文链接。

    年轻人,来手撸几种常见的限流算法!

    限流算法接口

    public interface RateLimiter {
       /**
        * 判断请求是否能够通过
        * @return 能通过返回true否则false
        */
       boolean tryAcquire();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    一,固定窗口限流算法

    首先维护一个计数器,将单位时间段当做一个窗口,计数器记录这个窗口接收请求的次数。

    • 当次数少于限流阀值,就允许访问,并且计数器+1
    • 当次数大于限流阀值,就拒绝访问。
    • 当前的时间窗口过去之后,计数器清零。

    假设单位时间是1秒,限流阀值为3。在单位时间1秒内,每来一个请求,计数器就加1,如果计数器累加的次数超过限流阀值3,后续的请求全部拒绝。等到1s结束后,计数器清0,重新开始计数。如下图:
    在这里插入图片描述
    国定窗口算法伪代码实现如下:

    public class CounterRateLimiter implements RateLimiter {
        // 每秒限制的请求数
        private final long permitsPerSecond;
    
        // 上一个窗口开始的时间
        private long timestamp = System.currentTimeMillis();
    
        // 计数器
        private int counter;
    
        public CounterRateLimiter(long permitsPerSecond) {
            this.permitsPerSecond = permitsPerSecond;
        }
    
        @Override
        public synchronized boolean tryAcquire() {
            long now = System.currentTimeMillis();
            // 窗口内请求数量小于阈值,更新计数放行,否则拒绝请求
            if (now - timestamp < 1000) {
                if (counter < permitsPerSecond) {
                    counter++;
                    return true;
                } else {
                    return false;
                }
            }
            // 时间窗口过期,重置计数器和时间戳
            counter = 0;
            timestamp = now;
            return true;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    但是,这种算法有一个很明显的临界问题:假设限流阀值为5个请求,单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s,分别并发5个请求。虽然都没有超过阀值,但是如果算0.8-1.2s,则并发数高达10,已经超过单位时间1s不超过5阀值的定义啦。
    在这里插入图片描述

    二,滑动窗口限流算法

    滑动窗口限流解决固定窗口临界值的问题。它将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。

    一张图解释滑动窗口算法,如下:
    在这里插入图片描述
    我们来看下滑动窗口是如何解决临界问题的?

    假设单位时间还是1s,滑动窗口算法把它划分为5个小周期,也就是滑动窗口(单位时间)被划分为5个小格子。每格表示0.2s。每过0.2s,时间窗口就会往右滑动一格。然后呢,每个小周期,都有自己独立的计数器,如果请求是0.83s到达的,0.8~1.0s对应的计数器就会加1。

    TIPS: 当滑动窗口的格子周期划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

    滑动窗口算法代码实现如下:

    public class SlidingWindowRateLimiter implements RateLimiter {
        // 每分钟限制的请求数
        private final long permitsPerMinute;
    
        // 计数器,k为当前窗口的开始时间值秒,value为当前窗口的计数
        private final TreeMap<Long, Integer> counters;
    
        public SlidingWindowRateLimiter(long permitsPerMinute) {
            this.permitsPerMinute = permitsPerMinute;
            this.counters = new TreeMap<>();
        }
    
        @Override
        public synchronized boolean tryAcquire() {
            // 获取当前时间所在的子窗口值;10s一个窗口
            long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / 10 * 10;
            // 获取当前窗口的请求总量
            int currentWindowCount = getCurrentWindowCount(currentWindowTime);
            if (currentWindowCount >= permitsPerMinute) {
                return false;
            }
            // 计数器加一
            counters.merge(currentWindowTime, 1, Integer::sum);
            return true;
        }
    
        /**
         * 获取当前窗口的所有请求数(并删除所有无效的子窗口计数器)
         *
         * @param currentWindowTime 当前子窗口时间
         * @return 当前窗口中的计数
         */
        private int getCurrentWindowCount(long currentWindowTime) {
            // 计算出窗口的开始位置时间
            long startTime = currentWindowTime - 50;
            int result = 0;
            // 遍历当前存储的计数器,删除无效的子窗口计数器,并累加当前窗口中的所有计数器之后
            Iterator<Map.Entry<Long, Integer>> entryIterator = counters.entrySet().iterator();
            while (entryIterator.hasNext()) {
                Map.Entry<Long, Integer> entry = entryIterator.next();
                if (entry.getKey() < startTime) {
                    entryIterator.remove();
                } else {
                    result += entry.getValue();
                }
            }
            return result;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    三,令牌桶限流算法

    面对突发流量的时候,我们可以使用令牌桶算法限流。

    令牌桶算法原理:

    • 有一个令牌管理员,根据限流大小,定速往令牌桶里放令牌。
    • 如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。
    • 系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑;
    • 如果拿不到令牌,就直接拒绝这个请求。

    在这里插入图片描述
    令牌桶算法代码实现如下:

    public class TokenBucketRateLimiter implements RateLimiter {
        // 令牌桶的容量
        private final long capacity;
        // 令牌发放速率
        private final long generatedPerSecond;
        // 最后一个令牌发放的时间
        private long lastTokenTime = System.currentTimeMillis();
        // 当前令牌数量
        private long currentTokens;
    
        public TokenBucketRateLimiter(long capacity, long generatedPerSecond) {
            this.capacity = capacity;
            this.generatedPerSecond = generatedPerSecond;
        }
    
        @Override
        public synchronized boolean tryAcquire() {
            /*
              计算当前令牌的数量
              请求时间在最后令牌产生的时间相差大于等于1s
              1.重新计算令牌桶中的令牌数量
              2. 将最后一个令牌发放时间重置为当前时间
             */
            long now = System.currentTimeMillis();
            if (now - lastTokenTime >= 1000) {
                long newPermits = (now - lastTokenTime) / 1000 * generatedPerSecond;
                currentTokens = Math.min(currentTokens + newPermits, capacity);
                lastTokenTime = now;
            }
            if (currentTokens > 0) {
                currentTokens--;
                return true;
            }
            return false;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    四,漏桶限流算法

    漏桶算法面对限流,就更加的柔性,不存在直接的粗暴拒绝。

    它的原理很简单,可以认为就是注水漏水的过程。往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。
    在这里插入图片描述

    • 流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的。
    • 桶的容量一般表示系统所能处理的请求数。
    • 如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)
    • 流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。

    漏桶算法代码实现如下:

    public class LeakyBucketRateLimiter implements RateLimiter {
        // 桶的容量
        private final int capacity;
        // 漏出的速率
        private final int permitsPerSecond;
        // 剩余水量
        private long leftWater;
        // 上次注入水的时间
        private long timeStamp = System.currentTimeMillis();
    
        public LeakyBucketRateLimiter(int capacity, int permitsPerSecond) {
            this.capacity = capacity;
            this.permitsPerSecond = permitsPerSecond;
        }
    
        @Override
        public synchronized boolean tryAcquire() {
            // 计算剩余水量
            long now = System.currentTimeMillis();
            long timeGap = (now - timeStamp) / 1000;
            leftWater = Math.max(0, leftWater - timeGap * permitsPerSecond);
            timeStamp = now;
            // 如果未满,则放行;否则限流
            if (leftWater < capacity){
                leftWater += 1;
                return true;
            }
            return false;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这并不是一个完整的漏桶算法的实现,以上代码中只是对流量是否会被抛弃进行校验,即tryAcquire返回true表示漏桶未满,否则表示漏桶已满丢弃请求。

    想要以恒定的速率漏出流量,通常还应配合一个FIFO队列来实现,当tryAcquire返回true时,将请求入队,然后再以固定频率从队列中取出请求进行处理。示例代码如下:

    @Test
    public void testLeakyBucketRateLimiter() throws InterruptedException {
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        ExecutorService singleThread = Executors.newSingleThreadExecutor();
    
        LeakyBucketRateLimiter rateLimiter = new LeakyBucketRateLimiter(20, 20);
        // 存储流量的队列
        Queue<Integer> queue = new LinkedList<>();
        // 模拟请求  不确定速率注水
        singleThread.execute(() -> {
            int count = 0;
            while (true) {
                count++;
                boolean flag = rateLimiter.tryAcquire();
                if (flag) {
                    queue.offer(count);
                    System.out.println(count + "--------流量被放行--------");
                } else {
                    System.out.println(count + "流量被限制");
                }
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    
        // 模拟处理请求 固定速率漏水
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            if (!queue.isEmpty()) {
                System.out.println(queue.poll() + "被处理");
            }
        }, 0, 100, TimeUnit.MILLISECONDS);
    
        // 保证主线程不会退出
        while (true) {
            Thread.sleep(10000);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    漏桶算法存在目的主要是用来平滑突发的流量,提供一种机制来确保网络中的突发流量被整合成平滑稳定的额流量。

    不过由于漏桶对流量的控制过于严格,在有些场景下不能充分使用系统资源,因为漏桶的漏出速率是固定的,即使在某一时刻下游能够处理更大的流量,漏桶也不允许突发流量通过。

    五,滑动日志限流算法

    滑动日志是一个比较“冷门”,但是确实好用的限流算法。滑动日志限速算法需要记录请求的时间戳,通常使用有序集合来存储,我们可以在单个有序集合中跟踪用户在一个时间段内所有的请求。

    假设我们要限制给定T时间内的请求不超过N,我们只需要存储最近T时间之内的请求日志,每当请求到来时判断最近T时间内的请求总数是否超过阈值。
    在这里插入图片描述![

    滑动日志算法代码实现如下:

    
    public class SlidingLogRateLimiter implements RateLimiter {
        // 每分钟限制的请求数
        private static final long PERMITS_PER_MINUTE = 60;
        // 请求日志计数器,k-为请求的时间(秒),value当前时间的请求数量
        private final TreeMap<Long, Integer> requestLogCountMap = new TreeMap<>();
    
        @Override
        public synchronized boolean tryAcquire() {
            // 最小时间粒度为秒
            long currentTimeStamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
            // 获取当前窗口的请求总数
            int currentWindowCount = getCurrentWindowCount(currentTimeStamp);
            if (currentWindowCount >= PERMITS_PER_MINUTE) {
                return false;
            }
            // 请求成功,将当前请求日志加入到日志计数器中
            requestLogCountMap.merge(currentTimeStamp, 1, Integer::sum);
            return false;
        }
    
        /**
         * 统计当前时间窗口内的请求数
         *
         * @param currentTimeStamp 当前时间
         * @return 请求数
         */
        private int getCurrentWindowCount(long currentTimeStamp) {
            // 计算出窗口的开始位置时间
            long startTime = currentTimeStamp - 59;
            // 遍历当前存储的计数器,删除无效的子窗口计数器,并累加当前窗口中的所有计数器之和
            return requestLogCountMap.entrySet()
                    .stream()
                    .filter(entry -> entry.getKey() >= startTime)
                    .mapToInt(Map.Entry::getValue)
                    .sum();
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    滑动日志能够避免突发流量,实现较为精准的限流;同样更加灵活,能够支持更加复杂的限流策略,如多级限流,每分钟不超过100次,每小时不超过300次,每天不超过1000次,我们只需要保存最近24小时所有的请求日志即可实现。

    灵活并不是没有代价的,带来的缺点就是占用存储空间要高于其他限流算法。

  • 相关阅读:
    idea-git配置用户名和邮箱
    nginx,域名绑定ipv6,本地能访问,但远程无法访问,如何解决?
    Java面向对象编程
    React高级特性之context
    7-25 念数字
    【MySQL】# 自定义变量、一行数据与多行的转换、IF函数
    迅为龙芯开发板开发板系统烧写-启动系统
    聚观早报 | 微软打造Xbox 手游商店;三星与TikTok推出StemDrop
    Go语言实现HTTP正向代理
    How to get active Profiles in Spring
  • 原文地址:https://blog.csdn.net/weixin_44991304/article/details/126527087