限流算法总结

计时器限流

计数器是一种比较简单的限流算法,用途比较广泛,在接口层面,很多地方使用这种方式限流。在一段时间内,进行计数,与阈值进行比较,到了时间临界点,将计数器清0 。
在这里插入图片描述
优点:实现简单
缺点:存在临界问题,如在12:01:00到12:01:58这段时间内没有用户请求,然后在12:01:59这一瞬时发出100个请求,OK,然后在12:02:00这一瞬时又发出了100个请求,那这某个1秒区间内,超过了每秒100次的请求阈值。

代码实现

public class CounterLimiter {

    private static long timeStamp = System.currentTimeMillis();

    // 1秒内允许请求数
    private static long limitCount = 100;

    // 请求间隔数
    private static long interval = 1000;

    // 请求限流数
    private static AtomicLong reqCount = new AtomicLong(0);

    public static boolean grant() {
        long now = System.currentTimeMillis();
        if (now < timeStamp + interval) {
            if (reqCount.get() < limitCount) {
                reqCount.incrementAndGet();
                return true;
            }
            return false;
        } else {
            timeStamp = System.currentTimeMillis();
            reqCount.set(0);
            return false;
        }
    }
}

滑动窗口限流

将时间划分为多个区间,在每个区间内每有一次请求就将计数器加一维持一个时间窗口,占据多个区间,每经过一个区间的时间,则抛弃最老的一个区间,并纳入最新的一个区间,如果当前窗口内区间的请求总和超过了限制数量,则本窗口内所有的请求都被丢弃。
在这里插入图片描述
在这里插入图片描述
优点:滑动窗口计数器是通过将窗口再细分,并且按照时间“滑动”,这种算法避免了固定窗口计数器带来的双倍突发请求。
缺点:时间区间的精度越高,算法所需的空间容量就越大。

代码实现

public class SlidingWindowLimiter {

    /**
     * 循环队列,就是装多个窗口用,该数量是windowSize的2倍
     */
    private AtomicInteger[] timeSlices;

    /**
     * 队列的总长度
     */
    private int timeSliceSize;

    /**
     * 每个时间片的时长,以毫秒为单位
     */
    private int timeMillisPerSlice;

    /**
     * 共有多少个时间片(即窗口长度)
     */
    private int windowSize;

    /**
     * 在一个完整窗口期内允许通过的最大阈值
     */
    private int threshold;

    /**
     * 该滑窗的起始创建时间,也就是第一个数据
     */
    private long beginTimestamp;

    /**
     * 最后一个数据的时间戳
     */
    private long lastAddTimestamp;

    public SlidingWindowLimiter(int timeMillisPerSlice, int windowSize, int threshold) {
        this.timeMillisPerSlice = timeMillisPerSlice;
        this.windowSize = windowSize;
        this.threshold = threshold;
        this.timeSliceSize = windowSize * 2;

        reset();
    }

   
    /**
     * 初始化队列
     */
    private void reset() {
        beginTimestamp = System.currentTimeMillis();
        if (timeSlices != null) {
            return;
        }
        AtomicInteger[] localTimeSlices =  new AtomicInteger[timeSliceSize];
        for (int i = 0; i < timeSliceSize; i++) {
            localTimeSlices[i] = new AtomicInteger(0);
        }
        timeSlices = localTimeSlices;
    }

    /**
     * 添加元素,模拟操作
     * @param count
     * @return
     */
    public boolean addCount(int count) {
        int index = locationIndex();
        System.out.println("index:" + index);
        clearFromIndex(index);
        int sum = 0;
        // 在当前时间片里继续+1
        sum += timeSlices[index].addAndGet(count);
        // 加上前面几个时间片里的数量
        for (int i= 1; i < windowSize; i++) {
            sum += timeSlices[(index - i + timeSliceSize) % timeSliceSize].get();
        }
        System.out.println(sum + "---" + threshold);

        lastAddTimestamp = System.currentTimeMillis();
        return sum >= threshold;

    }

    /**
     * 计算当前所在的时间片的位置
     * @return
     */
    private int locationIndex() {
        long now = System.currentTimeMillis();
        if ((now - lastAddTimestamp) > timeMillisPerSlice * windowSize) {
            reset();
        }
        return (int) (((now - beginTimestamp) / timeMillisPerSlice) % timeSliceSize);
    }


    /**
     * 然后清空自己前面windowSize到2*windowSize之间的数据格的数据
     * 譬如1秒分4个窗口,那么数组共计8个窗口
     * 当前index为5时,就清空6、7、8、1。然后把2、3、4、5的加起来就是该窗口内的总和
     * @param index
     */
    private void clearFromIndex(int index) {
        for (int i = 1; i <= windowSize; i++) {
            int j = index + i;
            if (j >= windowSize * 2) {
                j -= windowSize * 2;
            }
            timeSlices[j].set(0);
        }
    }
}

漏桶限流

将每个请求视作“水滴”放入“漏桶”进行存储,“漏桶”以固定速率向外“漏”出请求来执行如果“漏桶”空了则停止“漏水”,如果“漏桶”满了则多余的“水滴”会被直接丢弃。
在这里插入图片描述
优点:漏桶算法多使用队列实现,服务的请求会存到队列中,服务的提供方则按照固定的速率从过往队列中取出请求并执行,过多的请求则放在队列中排队或直接拒绝。
缺点:漏桶算法的缺陷也很明显,当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才被响应。

代码实现

public class LeakyBucketLimiter {
    // 时间刻度
    private static long timeStamp = System.currentTimeMillis();
    // 桶大小
    private static int bucketSize = 10;
    // 每ms流出的请求
    private static int rate = 1;
    // 当前的水量
    private static long count = 0;

    public static boolean grant() {
        long now = System.currentTimeMillis();
        // 计算出水量
        long out = (now - timeStamp) * rate;
        // 先执行漏水,计算剩余水量
        count = Math.max(0, count - out);
        timeStamp = now;
        if ((count + 1) < bucketSize) {
            // 桶未满,可以执行
            count ++;
            return true;
        }
        // 水满拒绝执行
        return false;
    }
}

令牌桶算法

令牌以固定速率生成,生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求可以执行,如果桶空了,那么尝试取令牌的请求会被直接丢弃
在这里插入图片描述
优点:令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法
缺点:牺牲小部分流量

代码实现

public class TokenBucketLimiter {
    public static long timeStamp = System.currentTimeMillis();
    // 桶的容量
    public static int bucketSize = 10;
    // 令牌放入速度
    public static int rate = 3;
    // 当前令牌数量
    public static long tokens = 0;

    public static boolean grant() {
        long now = System.currentTimeMillis();
        // 计算需要放桶中数量
        long in = (now - timeStamp) * rate;
        tokens = Math.min(bucketSize, tokens + in);
        // 判断桶中是否还有令牌
        if (tokens > 1) {
            tokens--;
            return true;
        }
        return false;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值