关于限流的理论知识,你掌握了么?

26 阅读6分钟

  当系统资源有限、处理能力有限时,限流可以对调用方的上游请求进行限制,以防止系统过载、确保系统稳定性和响应性。限流是一种重要的系统设计策略,旨在保护系统在面对高流量或潜在的服务滥用时保持稳定和可用。

常见的限流算法

固定窗口计数器

  固定窗口计数器是一种简单的限流策略,用于控制在给定时间窗口内对某资源的访问请求数量。

public class CounterLimiter {

    //初始时间
    private static long startTime = System.currentTimeMillis();

    //时间窗口限制
    private static final int interval = 1000;

    //限制通过请求
    private static int limit = 2;

    //请求计数
    private static AtomicInteger requestCount = new AtomicInteger(0);
    
    //计数判断,是否超出限制
    private synchronized boolean tryAcquire() {
        long nowTime = System.currentTimeMillis();
        //在时间区间之内
        if (nowTime < startTime + interval) {
            long count = requestCount.incrementAndGet();
            if (count <= limit) {
                return true;
            }
            return false;
        } else {
            requestCount.set(0);
            startTime = nowTime;
            return true;
        }
    }
}

  通过设定一个时间段interval(窗口)和允许的最大请求次数limit来实现限流。当请求次数达到设定的阈值时,任何进一步的请求都将被拒绝,直到下一个时间窗口开始。

image.png

  固定窗口计数器方式适用于实现简单的流量控制需求,存在请求分布不均的情况,其在窗口切换的瞬间,如果有大量请求同时到达,可能会导致两个连续时间窗口的请求总量暂时超过预期的最大允许值,因为计数器在窗口切换时重置。

滑动窗口计数器

  滑动窗口限流算法将原有的固定窗口时间划分为多个小窗口,在遇到下一个时间窗口之前调整窗口的范围,并对每个小窗口内的请求计数。

public class SlideCounterLimiter {

    // 初始时间
    private static long startTime = System.currentTimeMillis();

    // 时间窗口限制
    private static final int interval = 1000;

    // 限制通过请求
    private static final int limit = 2;

    // 窗口
    private static final Window[] windows = new Window[10];

    public SlideCounterLimiter() {
        long currentTimeMillis = System.currentTimeMillis();
        for (int i = 0; i < windows.length; i++) {
            windows[i] =new Window(currentTimeMillis,new AtomicInteger(0));
        }
    }

    public boolean tryAcquire() {
        long nowTime = System.currentTimeMillis();

        // 1. 计算当前时间窗口
        int currentIndex = (int) (nowTime % interval / (interval / windows.length));
        int requestCount = 0;

        for (int i = 0; i < windows.length; i++) {
            Window window = windows[i];
            if (nowTime>window.getTime()+interval) {
                window.getNumber().set(0);
                window.setTime(nowTime);
            }
            if (currentIndex == i && window.getNumber().get() < limit) {
                window.getNumber().incrementAndGet();
            }
            requestCount = requestCount+window.getNumber().get();
        }
        return requestCount<=limit;
    }


    private class Window {
        // 窗口开始时间
        private Long time;
        // 计数器
        private AtomicInteger number;

        public Window(long time, AtomicInteger number) {
            this.time = time;
            this.number = number;
        }

        public Long getTime() {
            return time;
        }

        public void setTime(Long time) {
            this.time = time;
        }

        public AtomicInteger getNumber() {
            return number;
        }

        public void setNumber(AtomicInteger number) {
            this.number = number;
        }
    }
    
}

  滑动窗口算法解决了固定窗口计数器算法的缺点。计数器的时间窗口是固定的,而滑动窗口的时间窗口是是动态的。

image.png

  滑动窗口算法可以一定程度缓解固定窗口限流存在的请求分布不均的问题,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。但是,它是基于过去的流量进行限制的,面对突然的大量请求时可能无法有效地限制流量,而且复杂度也高。

漏桶限流算法

  漏桶限流维护了一个固定容量的漏桶,请求以不定的速率流入漏桶,而漏桶以固定的速率流出。如果请求到达时,漏桶已满,则会触发拒绝策略

public class LeakyBucketLimiter {

    //桶的大小
    private static long capacity = 2;
    //流出速率,每秒两个
    private static long rate = 2;
    //开始时间
    private static long startTime = System.currentTimeMillis();
    //桶中剩余的水
    private static AtomicLong water = new AtomicLong();

    /**
     * true 代表放行,请求可已通过
     * false 代表限制,不让请求通过
     */
    public synchronized boolean tryAcquire() {
        long nowTime = System.currentTimeMillis();
        //如果桶的余量问0,直接放行
        if (water.get() == 0) {
            startTime = System.currentTimeMillis();
            water.set(1);
            return true;
        }
        //漏出水量,按照恒定的速度不断流出
        //漏出的水 = 过去的时间 *预设速率
        long outWater = (nowTime - startTime) * rate / 1000;
        //剩余的水量 = 上次遗留的水量 - 漏出去的水
        long remainder = water.get() - outWater;
        //防止出现<0的情况
        water.set(Math.max(0, remainder));

        //如果当前水小于容量,表示可以放行
        if (water.get() <capacity) {
            water.incrementAndGet();
            //设置新的开始时间
            startTime =nowTime;
            return true;
        } else {
            return false;
        }
    }
}

  漏桶的出水速度固定,也就是请求放行速度是固定的。

image.png

  漏桶算法保证了固定的流出速率,当大量数据突然到来时,漏桶算法处理能力有限。一旦输入速率超过了漏桶的容量,所有溢出的请求都会被丢弃(当然对于一些场景,我们可以将溢出的请求存到MQ中,后续再处理)。

令牌桶限流算法

  令牌桶限流算法它以固定的速度将令牌放入一个令牌桶中。当有新的请求到来时,需要从桶中取出一个令牌,如果桶中没有令牌,则请求被限制。

public class TokenBucketLimiter {

    //桶的大小
    private static long capacity = 2;
    //流入速率,每秒两个
    private static long rate = 2;
    //开始时间
    private static long startTime = System.currentTimeMillis();

    //桶中剩余的令牌
    private static AtomicLong token = new AtomicLong(0);

    /**
     * true 代表放行,请求可已通过
     * false 代表限制,不让请求通过
     */
    public synchronized boolean tryAcquire() {
        long nowTime = System.currentTimeMillis();
        //流入令牌,按照恒定的速度不断流入
        //流入的令牌 = 过去的时间 *预设速率
        long inToken = (nowTime - startTime) * rate / 1000;
        //剩余的令牌 = 上次遗留的令牌  + 流入的令牌
        long remainder = token.get() + inToken;
        //防止出现溢出的情况
        token.set(Math.min(capacity, remainder));
        //如果当前令牌>0,表示可以放行
        if (token.get() >0) {
            token.decrementAndGet();
            //设置新的开始时间
            startTime =nowTime;
            return true;
        } else {
            return false;
        }
    }
}

  当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么必须等待新的令牌被添加到桶中才能继续请求。

image.png

  令牌桶限流算法可以通过限制使用的令牌数量来控制数据的请求速率,可以应对一定的突发流量。

总结

  计数器算法实现比较简单,但是要考虑临界情况。漏桶算法和令牌桶算法,漏桶算法提供了比较严格的限流,令牌桶算法在限流之外,允许一定程度的突发流量。在实际开发中,我们并不需要这么精准地对流量进行控制,所以更推荐令牌桶算法。

  如果我们设置的流量峰值是每秒钟的请求量R,计数器算法会出现2R峰值,漏桶算法会始终限制R的峰值,而令牌桶算法允许大于R,但不会达到2R的峰值。