面试题之限流算法,从计数法到令牌桶

0、前言

所有技术都有它要解决的痛点。所以在介绍限流算法和具体场景之前我们要先得明确什么是限流,为什么要限流?然后从最简单的计数法到令牌桶说明几种限流算法的实现。

1、限流是什么

在开发的过程中,由于系统会面临高并发场景,或者也会遭遇一些恶意请求。为了保护我们的系统,保持系统的稳定性,我们要对请求进行限流。总结下来,限流就是:因为资源的稀缺或出于安全防范的目的,采取的自我保护的措施。限流是出于自我保护的目的,所以难以避免对用户体验造成影响。因此限流算法的目的就是在保持系统稳定性的同时,也尽量让用户有更好的体验。

一般来说,系统的吞吐量是有一个阈值的,如果请求量达到了这个阈值,我们就要对流量进行限制。

2、限流算法

计数限流

最简单的限流算法,思想很简单,直接计数。设置一个阈值,再设置一个计数器。当请求来的时候计数器加一,判断有没有超过阈值,请求处理过后,计数器减一。

java 复制代码
public class CounterLimiter {
    private int size; //阈值
    private AtomicInteger counter;//当前窗口的计数器
    public boolean tryAcquire(){
        if(counter.get() < size){
            counter.incrementAndGet();
            return true;
        }else{
            return false;
        }
    }
    public boolean tryRelease(){
        if(counter.get()>0){
            counter.decrementAndGet();
            return true;
        }else{
            return false;
        }
    }
}

缺点:

没有考虑时间问题。

固定窗口限流

相对于计数法,考虑了时间问题,引入了时间窗口的概念

在时间窗口内:

  • 请求次数小于阈值,允许访问并且计数器 +1;
  • 请求次数大于阈值,拒绝访问;

时间窗口过了之后,计数器清零;重新计数。

java 复制代码
public class CounterLimiter {
    private int timeWindow; //窗口大小
    private int limit; //阈值
    private long lastTime=System.currentTimeMillis();//上一次窗口时间
    private int counter;//当前窗口的计数器
    public boolean tryAcquire(){ 
        long now=System.currentTimeMillis();
        if(now-lastTime>timeWindow){ 
            //是否过了时间窗口
            counter=0;
            lastTime=now;
        }
        if(counter<limit){
            //是否超过阈值
            counter++;
            return true;
        }
        return false;
    }
}

缺点

没有考虑临界问题。

若不在时间窗口内的单位时间内请求量过大,固定的窗口没法处理。

滑动窗口限流

解决固定窗口临界值的问题,保证在任意时间窗口内都不会超过阈值。

限流规则:

  • 记录每次请求的时间
  • 统计每次请求的时间向前固定一段时间内的请求数
  • 统计的请求数小于阈值就记录这个请求的时间,并允许通过,反之拒绝。
java 复制代码
public class CounterLimiter {
    private int timeWindow; //窗口大小
    private int limit; //阈值
    private long lastTime=System.currentTimeMillis();//上一次窗口时间
    private final TreeMap<Long, Integer> counters = new TreeMap<>();//以时间为起点的计数器
    public boolean tryAcquire(){ 
        long now=System.currentTimeMillis();
        int counter=getCountTimeWindow(now);
        if(counter<limit){
            //是否超过阈值
            counters.get(now)++;
        	return true;
        }
        return false;
    }
    private int getCountTimeWindow(long currentTime) {
        //当前时间向前时间窗口内的请求数量
    }
}

缺点

固定窗口和滑动窗口都无法解决一个问题:

  • 当窗口中流量到达阈值时,流量会瞬间切断

我们所希望的是:

  • 当大量请求同时访问时,请求是均匀访问,而不是一次性的全打过来。

于是便有了漏桶算法限流。

漏桶限流

请求像水滴一样匀速的滴入桶中,服务端匀速的从桶中滴出请求进行处理。超过桶中阈值的请求会被拒绝。

限流规则:

  • 流入水滴代表请求,桶的容量系统所能处理请求数量的阈值。
  • 如果桶的容量满了,就达到限流的阀值,就会拒绝请求
  • 服务端匀速从桶内滴出请求进行处理
java 复制代码
public class BucketLimiter {
    private long rate;//滴入速率
    private long currentWater;//水桶内的水量
    private long lastTime;//上次注水时间
    private long capacity;//容量(阈值)
    public boolean tryAcquire(){ 
        long now=System.currentTimeMillis();
        long outWater=(now-lastTime)*rate;//流出的水量 =(当前时间-上次时间)* 滴入速率
        long currentWater=Math.max(0,currentWater-outWater);// 当前水量 = 桶内水量-流出的水量
        int counter=getCountTimeWindow(now);
         if (currentWater+1 <= capacity) {
            lastTime=now;//更新时间
            currentWater++; //水量+1
            return true;
        }
        return false;
    }
}

特点

主要是用来平滑突发的流量,请求访问的速率是不确定的,但是请求处理是匀速的。

缺点:对流量的控制过于严格,不能充分使用系统资源。漏桶算法对请求的处理过于死板,导致可能会浪费系统资源。明明可以更快的处理请求,但是只能匀速的去处理。

令牌桶限流算法

与漏桶算法不同的是:漏桶算法是匀速滴出,令牌桶算法是匀速的向桶内放令牌,由请求去申请令牌,申请到令牌的请求才可以被处理,否则会被拒绝。

限流规则:

  • 匀速向令牌桶内放入令牌
  • 若令牌数量超过阈值则抛弃令牌
  • 请求来了要向令牌桶申请获取令牌,若成功才可以被处理,否则被拒绝。
java 复制代码
public class BucketLimiter {
    private long rate;//令牌发放速率
    private long capacity;//令牌桶的容量
    private long counter;//令牌数量
    private long lastAcquireTime;//上次取令牌时间
    public boolean tryAcquire(){ 
        long now=System.currentTimeMillis();
        long newPermits=(now-lastAcquireTime)*rate; //这段时间放入的令牌数
        counter = Math.min(counter+newPermits,capacity);//当前令牌数
        if (counter > 0) {
            //令牌数够用,可以获取
            lastAcquireTime=now;
            counter--;
            return true;
        }
        return false;
    }
}

令牌桶算法特点

不像漏桶那样匀速处理请求,在应对突发流量的时候表现的更佳。

小结

对五种滑动窗口算法做了简单的介绍和分析。从最简单的计数器法,到令牌桶限流算法。当然每种算法都有它存在的意义,不是说令牌桶算法最好我们就只用它。简单的对各种限流算法做个小结。

  • 固定窗口实现简单。和漏斗算法相比,新来的请求也能够被马上处理到。但是流量曲线可能不够平滑。
  • 滑动窗口算法作为固定窗口算法的一种改进,有效解决了窗口临界值的问题。
  • 漏桶算法实现了对请求的匀速处理,让随机不稳定的流量以固定的速率流出。
  • 令牌桶算法改进了漏桶算法对资源的浪费,同时可以用来解决流量突发的问题。
相关推荐
爱吃生蚝的于勒36 分钟前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
ChoSeitaku6 小时前
链表循环及差集相关算法题|判断循环双链表是否对称|两循环单链表合并成循环链表|使双向循环链表有序|单循环链表改双向循环链表|两链表的差集(C)
c语言·算法·链表
Fuxiao___6 小时前
不使用递归的决策树生成算法
算法
我爱工作&工作love我6 小时前
1435:【例题3】曲线 一本通 代替三分
c++·算法
白-胖-子7 小时前
【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-统计数字
开发语言·c++·算法·蓝桥杯·等考·13级
workflower7 小时前
数据结构练习题和答案
数据结构·算法·链表·线性回归
好睡凯7 小时前
c++写一个死锁并且自己解锁
开发语言·c++·算法
Sunyanhui17 小时前
力扣 二叉树的直径-543
算法·leetcode·职场和发展
一个不喜欢and不会代码的码农7 小时前
力扣105:从先序和中序序列构造二叉树
数据结构·算法·leetcode
前端郭德纲7 小时前
浏览器是加载ES6模块的?
javascript·算法