限流常见算法

224 阅读4分钟

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

计数器算法

image.png

计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter,具体算法的示意图如下:

具体步骤:

  • 设置一个计数器统计单位时间内某个请求的访问量。
  • 在进入下一个单位时间内把计数器清零
  • 对于单位时间内超过计数器的访问,可以放入等待队列、直接拒接访问等策略

优点:简单
缺点:粗暴,易造成突刺现象。

漏斗算法

image.png

漏桶作为计量工具时,可用于流量整形和流量控制,漏桶的主要概念如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴(流出请求)
  • 如果桶是空的,则不需流出水滴
  • 可以以任意速率流入水滴到漏桶(流入请求)
  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(新流入的请求被拒绝),则漏桶容量是不变的

漏桶可以看做固定容量、固定流出速率的队列,漏桶限制的是请求的流出速率,漏桶中装的是请求。

优点:稳定速率
缺点:无法面对突发流量

令牌桶算法

image.png 令牌桶算法,其核心是要想通过限流器,必须拿到令牌。也就是说,只要我们能够限制发放令牌的速率,那么就能控制流速了。令牌桶算法的详细描述如下:

  • 令牌以固定的速率添加到令牌桶中,假设限流的速率是 r 个/秒,则令牌每 1/r 秒会添加一个;比如每秒放10个,每6秒放一个。
  • 假设令牌桶的容量是 b ,如果令牌桶已满,则新的令牌会被丢弃;
  • 每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。

这个算法中,限流的速率r还是比较容易理解的,但令牌桶的容量 b 该怎么理解呢?b 其实是burst的简写,意思是限流器允许的最大突发流量。比如b=10,而且令牌桶中的令牌已满,此时限流器允许10个请求同时通过限流器,当然只是突发流量而已,这10个请求会带走10个令牌,所以后续的流量只能按照速率 r 通过限流器。

优点:速率可变,能够面对突发流量
缺点:复杂度高

开源工具:Guava RateLimiter

RateLimiter是Guava的一个限流组件,它是基于令牌桶算法的,API非常简单

引入pom

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>20.0</version>
</dependency>

具体例子:

public class Test {

    public static void main(String[] args) throws Exception{
        //线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        //速率是每秒只有3个许可
        final RateLimiter rateLimiter = RateLimiter.create(3.0);

        for (int i = 0; i < 100; i++) {
            Thread.sleep(100);
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        boolean token = rateLimiter.tryAcquire();
                        if (token) {
                            System.out.println("token pass");
                        } else {
                            System.out.println("token refuse");
                        }

                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            };
            //执行线程
            exec.execute(runnable);
        }
        //退出线程池
        exec.shutdown();
    }
}

image.png

主要接口:

创建实例接口

    create(double permitsPerSecond)
    create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 预热模式创建,平滑过渡

获取permits

acquire(int permits) 阻塞模式获取,直到等到permits数量满足要求。
tryAcquire(long timeout, TimeUnit unit) 指定超时时间获取
tryAcquire(int permits) 非阻塞模式获取

本文已参与「新人创作礼」活动,一起开启掘金创作之路。