人人都能看懂的 6 种限流实现方案!(纯干货)

为了上班方便,去年我把本身在北郊的房子租出去了,搬到了南郊,这样离我上班的地方就近了,它为我节约了不少的时间成本,我能够用它来作不少有意义的事,最起码不会由于堵车而闹心了,幸福感直线上升。html

但即便这样,生活也有其余的烦恼。南郊的居住密度比较大,所以停车就成了头痛的事,我租的是路两边的非固定车位,每次只要下班回来,必定是没有车位停了,所以我只能和别人的车并排停着,但这样带来的问题是,我天天早上都要被挪车的电话给叫醒,心情天然就不用说了。java

但后来几天,我就慢慢变聪明了,我头天晚上停车的时候,会找次日限行的车并排停着,这样我次日就不用挪车了,这真是限行给我带来的“巨大红利”啊。redis

车辆限行就是一种生活中很常见的限流策略,他除了给我带来了以上的好处以外,还给咱们美好的生活环境带来了一丝改善,而且快速增加的私家车已经给咱们的交通带来了巨大的“负担”,若是再不限行,可能全部的车都要被堵在路上,这就是限流给咱们的生活带来的巨大好处。算法

从生活回到程序中,假设一个系统只能为 10W 人提供服务,忽然有一天由于某个热点事件,形成了系统短期内的访问量迅速增长到了 50W,那么致使的直接结果是系统崩溃,任何人都不能用系统了,显然只有少人数能用远比全部人都不能用更符合咱们的预期,所以这个时候咱们要使用「限流」了后端

限流分类

限流的实现方案有不少种,磊哥这里稍微理了一下,限流的分类以下所示:bash

  1. 合法性验证限流:好比验证码、IP 黑名单等,这些手段能够有效的防止恶意攻击和爬虫采集;
  2. 容器限流:好比 Tomcat、Nginx 等限流手段,其中 Tomcat 能够设置最大线程数(maxThreads),当并发超过最大线程数会排队等待执行;而 Nginx 提供了两种限流手段:一是控制速率,二是控制并发链接数;
  3. 服务端限流:好比咱们在服务器端经过限流算法实现限流,此项也是咱们本文介绍的重点。

合法性验证限流为最常规的业务代码,就是普通的验证码和 IP 黑名单系统,本文就不作过多的叙述了,咱们重点来看下后两种限流的实现方案:容器限流和服务端限流。服务器

容器限流

Tomcat 限流

Tomcat 8.5 版本的最大线程数在 conf/server.xml 配置中,以下所示:并发

<Connector port="8080" protocol="HTTP/1.1"
          connectionTimeout="20000"
          maxThreads="150"
          redirectPort="8443" />
复制代码

其中 maxThreads 就是 Tomcat 的最大线程数,当请求的并发大于此值(maxThreads)时,请求就会排队执行,这样就完成了限流的目的。框架

小贴士:maxThreads 的值能够适当的调大一些,此值默认为 150(Tomcat 版本 8.5.42),但这个值也不是越大越好,要看具体的硬件配置,须要注意的是每开启一个线程须要耗用 1MB 的 JVM 内存空间用于做为线程栈之用,而且线程越多 GC 的负担也越重。最后须要注意一下,操做系统对于进程中的线程数有必定的限制,Windows 每一个进程中的线程数不容许超过 2000,Linux 每一个进程中的线程数不容许超过 1000。分布式

Nginx 限流

Nginx 提供了两种限流手段:一是控制速率,二是控制并发链接数。

控制速率

咱们须要使用 limit_req_zone 用来限制单位时间内的请求数,即速率限制,示例配置以下:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server { 
    location / { 
        limit_req zone=mylimit;
    }
}
复制代码

以上配置表示,限制每一个 IP 访问的速度为 2r/s,由于 Nginx 的限流统计是基于毫秒的,咱们设置的速度是 2r/s,转换一下就是 500ms 内单个 IP 只容许经过 1 个请求,从 501ms 开始才容许经过第 2 个请求。

咱们使用单 IP 在 10ms 内发并发送了 6 个请求的执行结果以下:

img

从以上结果能够看出他的执行符合咱们的预期,只有 1 个执行成功了,其余的 5 个被拒绝了(第 2 个在 501ms 才会被正常执行)。

速率限制升级版

上面的速率控制虽然很精准可是应用于真实环境未免太苛刻了,真实状况下咱们应该控制一个 IP 单位总时间内的总访问次数,而不是像上面那么精确但毫秒,咱们可使用 burst 关键字开启此设置,示例配置以下:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server { 
    location / { 
        limit_req zone=mylimit burst=4;
    }
}
复制代码

burst=4 表示每一个 IP 最多容许4个突发请求,若是单个 IP 在 10ms 内发送 6 次请求的结果以下:

img

从以上结果能够看出,有 1 个请求被当即处理了,4 个请求被放到 burst 队列里排队执行了,另外 1 个请求被拒绝了。

控制并发数

利用 limit_conn_zonelimit_conn 两个指令便可控制并发数,示例配置以下:

limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
    ...
    limit_conn perip 10;
    limit_conn perserver 100;
}
复制代码

其中 limit_conn perip 10 表示限制单个 IP 同时最多能持有 10 个链接;limit_conn perserver 100 表示 server 同时能处理并发链接的总数为 100 个。

小贴士:只有当 request header 被后端处理后,这个链接才进行计数。

服务端限流

服务端限流须要配合限流的算法来执行,而算法至关于执行限流的“大脑”,用于指导限制方案的实现。

有人看到「算法」两个字可能就晕了,以为很深奥,其实并非。算法就至关于操做某个事务的具体实现步骤汇总,其实并不难懂,不要被它的表象给吓到哦~

限流的常见算法有如下三种:

  1. 时间窗口算法
  2. 漏桶算法
  3. 令牌算法

接下来咱们分别看来。

1.时间窗口算法

所谓的滑动时间算法指的是以当前时间为截止时间,往前取必定的时间,好比往前取 60s 的时间,在这 60s 以内运行最大的访问数为 100,此时算法的执行逻辑为,先清除 60s 以前的全部请求记录,再计算当前集合内请求数量是否大于设定的最大请求数 100,若是大于则执行限流拒绝策略,不然插入本次请求记录并返回能够正常执行的标识给客户端。

滑动时间窗口以下图所示:

其中每一小个表示 10s,被红色虚线包围的时间段则为须要判断的时间间隔,好比 60s 秒容许 100 次请求,那么红色虚线部分则为 60s。

咱们能够借助 Redis 的有序集合 ZSet 来实现时间窗口算法限流,实现的过程是先使用 ZSet 的 key 存储限流的 ID,score 用来存储请求的时间,每次有请求访问来了以后,先清空以前时间窗口的访问量,统计如今时间窗口的个数和最大容许访问量对比,若是大于等于最大访问量则返回 false 执行限流操做,负责容许执行业务逻辑,而且在 ZSet 中添加一条有效的访问记录,具体实现代码以下。

咱们借助 Jedis 包来操做 Redis,实如今 pom.xml 添加 Jedis 框架的引用,配置以下:

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
</dependency>
复制代码

具体的 Java 实现代码以下:

import redis.clients.jedis.Jedis;

public class RedisLimit {
    // Redis 操做客户端
    static Jedis jedis = new Jedis("127.0.0.1", 6379);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 15; i++) {
            boolean res = isPeriodLimiting("java", 3, 10);
            if (res) {
                System.out.println("正常执行请求:" + i);
            } else {
                System.out.println("被限流:" + i);
            }
        }
        // 休眠 4s
        Thread.sleep(4000);
        // 超过最大执行时间以后,再从发起请求
        boolean res = isPeriodLimiting("java", 3, 10);
        if (res) {
            System.out.println("休眠后,正常执行请求");
        } else {
            System.out.println("休眠后,被限流");
        }
    }

    /**
     * 限流方法(滑动时间算法)
     * @param key      限流标识
     * @param period   限流时间范围(单位:秒)
     * @param maxCount 最大运行访问次数
     * @return
     */
    private static boolean isPeriodLimiting(String key, int period, int maxCount) {
        long nowTs = System.currentTimeMillis(); // 当前时间戳
        // 删除非时间段内的请求数据(清除老访问数据,好比 period=60 时,标识清除 60s 之前的请求记录)
        jedis.zremrangeByScore(key, 0, nowTs - period * 1000);
        long currCount = jedis.zcard(key); // 当前请求次数
        if (currCount >= maxCount) {
            // 超过最大请求次数,执行限流
            return false;
        }
        // 未达到最大请求数,正常执行业务
        jedis.zadd(key, nowTs, "" + nowTs); // 请求记录 +1
        return true;
    }
}
复制代码

以上程序的执行结果为:

正常执行请求:0

正常执行请求:1

正常执行请求:2

正常执行请求:3

正常执行请求:4

正常执行请求:5

正常执行请求:6

正常执行请求:7

正常执行请求:8

正常执行请求:9

被限流:10

被限流:11

被限流:12

被限流:13

被限流:14

休眠后,正常执行请求

此实现方式存在的缺点有两个:

  • 使用 ZSet 存储有每次的访问记录,若是数据量比较大时会占用大量的空间,好比 60s 容许 100W 访问时;
  • 此代码的执行非原子操做,先判断后增长,中间空隙可穿插其余业务逻辑的执行,最终致使结果不许确。

2.漏桶算法

漏桶算法的灵感源于漏斗,以下图所示:

滑动时间算法有一个问题就是在必定范围内,好比 60s 内只能有 10 个请求,当第一秒时就到达了 10 个请求,那么剩下的 59s 只能把全部的请求都给拒绝掉,而漏桶算法能够解决这个问题。

漏桶算法相似于生活中的漏斗,不管上面的水流倒入漏斗有多大,也就是不管请求有多少,它都是以均匀的速度慢慢流出的。当上面的水流速度大于下面的流出速度时,漏斗会慢慢变满,当漏斗满了以后就会丢弃新来的请求;当上面的水流速度小于下面流出的速度的话,漏斗永远不会被装满,而且能够一直流出。

漏桶算法的实现步骤是,先声明一个队列用来保存请求,这个队列至关于漏斗,当队列容量满了以后就放弃新来的请求,而后从新声明一个线程按期从任务队列中获取一个或多个任务进行执行,这样就实现了漏桶算法。

上面咱们演示 Nginx 的控制速率其实使用的就是漏桶算法,固然咱们也能够借助 Redis 很方便的实现漏桶算法。

咱们可使用 Redis 4.0 版本中提供的 Redis-Cell 模块,该模块使用的是漏斗算法,而且提供了原子的限流指令,并且依靠 Redis 这个天生的分布式程序就能够实现比较完美的限流了。

Redis-Cell 实现限流的方法也很简单,只须要使用一条指令 cl.throttle 便可,使用示例以下:

> cl.throttle mylimit 15 30 60
1)(integer)0 # 0 表示获取成功,1 表示拒绝
2)(integer)15 # 漏斗容量
3)(integer)14 # 漏斗剩余容量
4)(integer)-1 # 被拒绝以后,多长时间以后再试(单位:秒)-1 表示无需重试
5)(integer)2 # 多久以后漏斗彻底空出来
复制代码

其中 15 为漏斗的容量,30 / 60s 为漏斗的速率。

3.令牌算法

在令牌桶算法中有一个程序以某种恒定的速度生成令牌,并存入令牌桶中,而每一个请求须要先获取令牌才能执行,若是没有获取到令牌的请求能够选择等待或者放弃执行,以下图所示:

咱们可使用 Google 开源的 guava 包,很方便的实现令牌桶算法,首先在 pom.xml 添加 guava 引用,配置以下:

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.2-jre</version>
</dependency>
复制代码

具体实现代码以下:

import com.google.common.util.concurrent.RateLimiter;

import java.time.Instant;

/**
 * Guava 实现限流
 */
public class RateLimiterExample {
    public static void main(String[] args) {
        // 每秒产生 10 个令牌(每 100 ms 产生一个)
        RateLimiter rt = RateLimiter.create(10);
        for (int i = 0; i < 11; i++) {
            new Thread(() -> {
                // 获取 1 个令牌
                rt.acquire();
                System.out.println("正常执行方法,ts:" + Instant.now());
            }).start();
        }
    }
}
复制代码

以上程序的执行结果为:

正常执行方法,ts:2020-05-15T14:46:37.175Z

正常执行方法,ts:2020-05-15T14:46:37.237Z

正常执行方法,ts:2020-05-15T14:46:37.339Z

正常执行方法,ts:2020-05-15T14:46:37.442Z

正常执行方法,ts:2020-05-15T14:46:37.542Z

正常执行方法,ts:2020-05-15T14:46:37.640Z

正常执行方法,ts:2020-05-15T14:46:37.741Z

正常执行方法,ts:2020-05-15T14:46:37.840Z

正常执行方法,ts:2020-05-15T14:46:37.942Z

正常执行方法,ts:2020-05-15T14:46:38.042Z

正常执行方法,ts:2020-05-15T14:46:38.142Z

从以上结果能够看出令牌确实是每 100ms 产生一个,而 acquire() 方法为阻塞等待获取令牌,它能够传递一个 int 类型的参数,用于指定获取令牌的个数。它的替代方法还有 tryAcquire(),此方法在没有可用令牌时就会返回 false 这样就不会阻塞等待了。固然 tryAcquire() 方法也能够设置超时时间,未超过最大等待时间会阻塞等待获取令牌,若是超过了最大等待时间,尚未可用的令牌就会返回 false。

注意:使用 guava 实现的令牌算法属于程序级别的单机限流方案,而上面使用 Redis-Cell 的是分布式的限流方案。

总结

本文提供了 6 种具体的实现限流的手段,他们分别是:Tomcat 使用 maxThreads 来实现限流;Nginx 提供了两种限流方式,一是经过 limit_req_zoneburst 来实现速率限流,二是经过 limit_conn_zonelimit_conn 两个指令控制并发链接的总数。最后咱们讲了时间窗口算法借助 Redis 的有序集合能够实现,还有漏桶算法可使用 Redis-Cell 来实现,以及令牌算法能够解决 Google 的 guava 包来实现。

须要注意的是借助 Redis 实现的限流方案可用于分布式系统,而 guava 实现的限流只能应用于单机环境。若是你嫌弃服务器端限流麻烦,甚至能够在不改代码的状况下直接使用容器限流(Nginx 或 Tomcat),但前提是能知足你的业务需求。

好了,文章到这里就结束了,期待咱们下期再会~

最后的话

原创不易,若是以为本文对你有用,请随手点击一个「」,这是对做者最大的支持与鼓励,谢谢你!

参考 & 鸣谢

www.cnblogs.com/biglittlean…

关注公众号「Java中文社群」回复“干货”,获取 50 篇原创干货 Top 榜

相关文章
相关标签/搜索