当前位置 博文首页 > 面试官:来,年轻人!请手撸5种常见限流算法!

    面试官:来,年轻人!请手撸5种常见限流算法!

    作者:浪人~ 时间:2021-01-09 06:04

    填坑限流算法!练就计数器,滑动窗口,令牌桶,漏桶,滑动日志五毒神掌。
    1. 瞬时流量过高,服务被压垮?
    2. 恶意用户高频光顾,导致服务器宕机?
    3. 消息消费过快,导致数据库压力过大,性能下降甚至崩溃?
      ……

    在高并发系统中,出于系统保护角度考虑,通常会对流量进行限流;不但在工作中要频繁使用,而且也是面试中的高频考点。

    今天我们将图文并茂地对常见的限流算法分别进行介绍,通过各个算法的特点,给出限流算法选型的一些建议,并给出Java语言实现的代码示例。

    01 固定窗口

    固定窗口又称固定窗口(又称计数器算法,Fixed Window)限流算法,是最简单的限流算法,通过在单位时间内维护的计数器来控制该时间单位内的最大访问量。

    假设限制每分钟请求量不超过60,设置一个计数器,当请求到达时如果计数器到达阈值,则拒绝请求,否则计数器加1;每分钟重置计数器为0。代码实现如下:

    public class CounterRateLimiter extends MyRateLimiter {
        /**
         * 每秒限制请求数
         */

        private final long permitsPerSecond;
        /**
         * 上一个窗口的开始时间
         */

        public 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;
        }
    }

    固定窗口最大的优点在于易于实现;并且内存占用小,我们只需要存储时间窗口中的计数即可;它能够确保处理更多的最新请求,不会因为旧请求的堆积导致新请求被饿死。当然也面临着临界问题,当两个窗口交界处,瞬时流量可能为2n。

    02 滑动窗口

    为了防止瞬时流量,可以把固定窗口近一步划分成多个格子,每次向后移动一小格,而不是固定窗口大小,这就是滑动窗口(Sliding Window)。

    比如每分钟可以分为6个10秒中的单元格,每个格子中分别维护一个计数器,窗口每次向前滑动一个单元格。每当请求到达时,只要窗口中所有单元格的计数总和不超过阈值都可以放行。TCP协议中数据包的传输,同样也是采用滑动窗口来进行流量控制。

    实现如下:

    public class SlidingWindowRateLimiter extends MyRateLimiter {
        /**
         * 每分钟限制请求数
         */

        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;
            }
            // 计数器 + 1
            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>> iterator = counters.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<Long, Integer> entry = iterator.next();
                if (entry.getKey() < startTime) {
                    iterator.remove();
                } 
    下一篇:没有了