关于java:6种限流实现附代码通俗易懂

49次阅读

共计 6776 个字符,预计需要花费 17 分钟才能阅读完成。

限流是一种管制拜访速率的策略,用于限度零碎、服务或 API 接口的申请频率或数量。它的目标是为了爱护零碎免受过多申请的影响,避免零碎因过载而解体或变得不可用。限流是一种重要的性能优化和资源爱护机制。

限流的益处有以下几个:

  • 爱护零碎稳定性:如果零碎承受太多申请,超出了其解决能力,可能导致系统解体或响应工夫急剧减少,从而影响用户体验。限流能够帮忙管制申请速率,确保零碎稳固运行。
  • 爱护零碎可用性:有些资源可能是无限的,如数据库连贯、网络带宽、内存等。通过限度对这些资源的拜访,能够避免它们被耗尽,从而爱护零碎的可用性。
  • 避免歹意攻打:限流能够缩小歹意攻打和滥用系统资源的危险。例如,避免 DDoS(分布式拒绝服务)攻打或歹意爬虫拜访网站。
  • 偏心分配资源:对于多个客户或用户,限流能够确保资源偏心调配。每个客户都有限度的拜访机会,而不会被某个客户垄断。
  • 防止雪崩效应:当零碎中的一个组件或服务产生故障时,可能会导致大量申请涌入其余失常的组件或服务,进一步加剧零碎负载,限流能够避免这种雪崩效应。

    限流分类

    限流的实现计划有很多种,磊哥这里略微理了一下,限流的分类如下所示:

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

合法性验证限流为最惯例的业务代码,就是一般的验证码和 IP 黑名单零碎,本文就不做过多的叙述了,咱们重点来看下后两种限流的实现计划:容器限流和服务端限流。

一、容器限流

1.1 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。

1.2 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 个申请的执行后果如下:

从以上后果能够看出他的执行合乎咱们的预期,只有 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 次申请的后果如下:

从以上后果能够看出,有 1 个申请被立刻解决了,4 个申请被放到 burst 队列里排队执行了,另外 1 个申请被回绝了。

管制并发数

利用 limit_conn_zone 和 limit_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. 令牌算法

接下来咱们别离看来。

2.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.1 漏桶算法

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

滑动工夫算法有一个问题就是在肯定范畴内,比方 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 为漏斗的速率。

2.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:2023-05-15T14:46:37.175Z

失常执行办法,ts:2023-05-15T14:46:37.237Z

失常执行办法,ts:2023-05-15T14:46:37.339Z

失常执行办法,ts:2023-05-15T14:46:37.442Z

失常执行办法,ts:2023-05-15T14:46:37.542Z

失常执行办法,ts:2023-05-15T14:46:37.640Z

失常执行办法,ts:2023-05-15T14:46:37.741Z

失常执行办法,ts:2023-05-15T14:46:37.840Z

失常执行办法,ts:2023-05-15T14:46:37.942Z

失常执行办法,ts:2023-05-15T14:46:38.042Z

失常执行办法,ts:2023-05-15T14:46:38.142Z

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

留神:应用 guava 实现的令牌算法属于程序级别的单机限流计划,而下面应用 Redis-Cell 的是分布式的限流计划。

小结

本文提供了 6 种具体的实现限流的伎俩,他们别离是:Tomcat 应用 maxThreads 来实现限流;Nginx 提供了两种限流形式,一是通过 limit_req_zone 和 burst 来实现速率限流,二是通过 limit_conn_zonelimit_conn 两个指令管制并发连贯的总数。最初咱们讲了工夫窗口算法借助 Redis 的有序汇合能够实现,还有漏桶算法能够应用 Redis-Cell 来实现,以及令牌算法能够解决 Google 的 guava 包来实现。

须要留神的是借助 Redis 实现的限流计划可用于分布式系统,而 guava 实现的限流只能利用于单机环境。如果你厌弃服务器端限流麻烦,甚至能够在不改代码的状况下间接应用容器限流(Nginx 或 Tomcat),但前提是能满足你的业务需要。

好了,本节到这里就完结了,下期咱们再会~

参考 & 鸣谢

https://www.cnblogs.com/biglittleant/p/8979915.html

本文已收录到我的面试小站 www.javacn.site,其中蕴含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、音讯队列等模块。

正文完
 0