常见的几种限流算法

273 阅读3分钟

限流

限流已经是目前互联网项目的常规手段了,主要作用是为了在系统遇到大量突发的请求时,通过拒绝或丢弃超出承载能力以外的请求,从而达到保护系统的目的。我们下面主要讨论几种主要的限流算法。

计数算法

顾名思义,计数就是简单的计算单位时间内的请求总数,超过上限的请求直接拒绝

示例代码:

public class SimpleCountLimiter {
        private int limit = 10;
        private int window = 1000;
        private int reqCount = 0;
        private volatile long start = System.currentTimeMillis();

        public SimpleCountLimiter(int limit, int window) {
                this.limit = limit;
                this.window = window;
        }

        public synchronized boolean acquire() {
            int cur = reqCount++;
            long ts = System.currentTimeMillis();
            if (cur < limit) {
                return true;
            }
            if (ts - start > window) {
                start = ts;
                reqCount = 1;
                return true;
            }
            return false;

        }
    }

直接计数有啥问题呢? 假如我的突发请求集中在两个时间窗口的交汇处,那么就可能在一个时间窗口内接收两倍的请求,超出了我们的系统承受范围。

滑动窗口

滑动窗口可以解决上面简单计数所面临的问题,它将整个窗口W划分为N段,每个请求通过计算落在其中某个段内,每经过W/N时间则窗口向右滑动一格。 这样的话,段划分的越多,则请求限流的越平滑。如果只划分一段,其实就是我们上面简单计数算法了。

示例代码:

public class SlidingWindowLimiter {

        //窗口大小  单位 ms
        private int window = 1000;
        //窗口划分格数
        private int slot = 5;
        //时间窗口内请求上限
        private int limit = 50;

        //窗口内每格请求数
        private final int[] reqCount = new int[slot];
        //每格时间
        private final long timePerSlot = window / slot;

        //当前指向slot
        private int index = 0;
        //时间窗口开始时间
        private long start = System.currentTimeMillis();

        public SlidingWindowLimiter() {
        }

        public SlidingWindowLimiter(int window, int slot, int limit) {
            this.window = window;
            this.slot = slot;
            this.limit = limit;
        }

        public synchronized boolean acquire() {
            long cur = System.currentTimeMillis();

            //需要滑动的slot数量,如果当前时间减去滑动窗口开始时间小于滑动窗口,证明不需要移动。
            long slideSlot = Math.max(cur - start - window, 0) / this.timePerSlot;

            //等于0不需要滑动
            if (slideSlot > 0) {
                //如果需要滑动的slot数量大于划分的slot,则取默认的slot大小
                long move = Math.min(slideSlot, slot);

                //每滑动一格,将当前指向的格内请求数清空
                for (int i = 0; i < move; i++) {
                    index = (index + 1) % slot;
                    reqCount[index] = 0;
                }
                //更新滑动窗口开始时间
                start = start + move * timePerSlot;
            }

            int sum = 0;
            for (int j : reqCount) {
                sum += j;
            }

            if (sum >= limit) {
                return false;
            }
            reqCount[index]++;
            return true;
        }

    }

漏斗算法

漏斗算法和消息队列相似,想象一下漏斗是怎么工作的,上面一个巨大的开口,不管放多少沙子进去,下面的出口永远是按指定的速率流出.漏洞算法也是这样的:维护一个指定大小的队列,按指定的速率消费队列来执行请求,当请求大于队列大小时,则拒绝请求。

示例代码:

public class FunnelLimiter {

        private final int limit;
        private final int rate;

        private final ArrayBlockingQueue<Request> taskQueue;


        public FunnelLimiter(int limit, int rate) {
            this.limit = limit;
            this.rate = rate;
            taskQueue = new ArrayBlockingQueue<>(this.limit);
            new Thread(() -> {
                while (true) {
                    try {
                        handleRequest(taskQueue.take());
                        Thread.sleep(1000 / this.rate);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }).start();
        }

        private void handleRequest(Request request) {
            //do request
            System.out.println("do request:" + request);
        }

        public boolean acquire(Request request) {
            return taskQueue.offer(request);
        }

        @Data
        @AllArgsConstructor
        public static class Request {
            private long id;
        }

    }

令牌桶算法

令牌桶算法和漏斗类似,漏斗是按一定的速率处理请求,而令牌桶是按一定的速率生成令牌,请求只有获取到令牌才可以被执行,否则丢弃。

示例代码:

public class RateLimiter {

        private final int limit;

        private final int rate;

        private final int init;

        private final AtomicInteger bucket;

        public RateLimiter(int limit, int rate, int init) {
            this.limit = limit;
            this.rate = rate;
            this.init = init;
            bucket = new AtomicInteger(this.init);
            new Thread(() -> {
                while (true) {
                    int count = bucket.get();
                    if (count < this.limit) {
                        bucket.incrementAndGet();
                    }
                    try {
                        Thread.sleep(1000 / this.rate);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }).start();
        }

        public synchronized boolean acquire() {
            int remained = bucket.get();
            if (remained <= 0) {
                return false;
            }
            bucket.decrementAndGet();
            return true;
        }
    }

令牌桶与漏斗算法的区别在于:令牌桶算法只要获取到令牌,则可以执行请求,意味着在一定时间内,是允许处理部分突发流量的。而漏斗算法是按指定的速率来消费请求,所以它主要是用来平滑流量,将突发流量转换为稳定的流量,上面说到他和消费队列类似,这其实就是MQ的流量削峰。