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

这是我的第 195 期分享

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

为了上班方便,去年我把自己在北郊的房子租出去了,搬到了南郊,这样离我上班的地方就近了,它为我节约了很多的时间成本,我可以用它来做很多有意义的事,最起码不会因为堵车而闹心了,幸福感直线上升。

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

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

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

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

限流分类

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

  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 个请求的执行结果如下:

从以上结果可以看出他的执行符合我们的预期,只有 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. 令牌算法

接下来我们分别看来。

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);}}// 休眠 4sThread.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); // 请求记录 +1return 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_zone 和 burst 来实现速率限流,二是通过 limit_conn_zonelimit_conn 两个指令控制并发连接的总数。最后我们讲了时间窗口算法借助 Redis 的有序集合可以实现,还有漏桶算法可以使用 Redis-Cell 来实现,以及令牌算法可以解决 Google 的 guava 包来实现。

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

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

最后的话

原创不易,如果觉得本文对你有用,请随手点击一个「在看」,这是对作者最大的支持与鼓励,谢谢你!

参考 & 鸣谢

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

一口气说出 6 种延时队列的实现方法,面试官满意的笑了

String性能提升10倍的几个方法!(源码+原理分析)

关注公众号发送”进群“,磊哥拉你进读者群。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546134.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

測试新浪微博@小冰 为代码机器人的一些方法

微软的微信小冰被腾讯封杀之后,如今移民到了新浪微博; 小冰 这里贴一些眼下有效的用来识别是这是"机器"而不是有正常人类智商的代码的方法: 1. 在正常的文字中夹杂其他符号,确保不存在有意义的连续的词汇,人眼能够分辨,机器不知所云而会露馅: 比方: ^^^小v冰^^^-…

秒建一个后台管理系统?用这5个开源免费的Java项目就够了

这是我的第 196 期分享作者 | Guide来源 | JavaGuide&#xff08;ID&#xff1a;JavaGuide&#xff09; 分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;大家好&#xff0c;我是 Guide 哥&#xff0c;一个三观比主角还正的技术人。今天推荐几个 Java 项目…

读《白帽子讲Web安全》之客户端脚本安全(一)

2019独角兽企业重金招聘Python工程师标准>>> 【第2章 浏览器安全】 1、同源策略&#xff08;Same Origin Policy&#xff09;是一种约定&#xff0c;它是浏览器最核心也最基本的安全功能。 浏览器的同源策略&#xff0c;限制了来自不同源的“document”或脚本&…

RocketMQ一行代码造成消息发送失败

这是我的第 198 期分享作者 | 丁威来源 | 中间件兴趣圈&#xff08;ID&#xff1a;dingwpmz_zjj&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;1、问题现象首先接到项目反馈使用 RocketMQ 会出现如下错误&#xff1a;错误信息关键点&#xf…

Java 中的 String 有没有长度限制?

这是我的第 199 期分享作者 | Hollis来源 | Hollis&#xff08;ID&#xff1a;hollischuang&#xff09; 分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;关于String有没有长度限制的问题&#xff0c;我之前单独写过一篇文章分析过&#xff0c;最近我又抽…

开源 免费 java CMS - FreeCMS2.1 菜单管理

2019独角兽企业重金招聘Python工程师标准>>> 项目地址&#xff1a;http://www.freeteam.cn/ 菜单管理 FreeCMS在设计时定位于面向二次开发友好&#xff0c;所以FreeCMS提供了菜单管理功能&#xff0c;二次开发人员可以自由增加新的功能菜单到FreeCMS。 为了让后台…

本来想用“{{”秀一波,结果却导致了内存溢出!

这是我的第 200 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;生活中的尴尬无处不在&#xff0c;有时候你只是想简单的装一把&#xff0c;但某些“老同志”总是在不…

局部变量竟然比全局变量快 5 倍?

这是我的第 201 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;喽&#xff0c;大家好&#xff0c;磊哥的性能优化篇又来了&#xff01;其实写这个性能优化类的文章初…

FreeMarker笔记 前言第1章 入门

简介 简介 FreeMarker是一款模板引擎&#xff1a;一种基于模板的、用来生成输出文本&#xff08;任何来自于HTML格式的文本用来自动生成源代码&#xff09;的通用工具。它是为Java程序员提供的一个开发包或者说是类库。它不是面向最终用户&#xff0c;而是为程序员提供的可以嵌…

厉害了,3万字的MySQL精华总结 + 面试100问!

这是我的第 202 期分享作者 | 派大新来源 | JavaKeeper&#xff08;ID&#xff1a;JavaKeeper&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;❝写在之前&#xff1a;不建议那种上来就是各种面试题罗列&#xff0c;然后背书式的去记忆&#x…

网页视频播放器代码大全 + 21个为您的站点和博客提供的免费视频播放器

推荐 使用 极酷 Web在线播放器。网页中嵌入视频代码综合全然版 1.avi格式 代码片断例如以下&#xff1a;  程序代码 <objectid"video"width"400"height"200"border"0"classid"clsid:CFCDAA03-8BE4-11cf-B84B-0020AFBBCCFA&q…

漫话:为什么计算机起始时间是1970年1月1日?

这是我的第 203 期分享作者 | 漫画编程来源 | 漫画编程&#xff08;ID&#xff1a;mhcoding&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;问题复现1970-01-01对于开发者来说都是不陌生的&#xff0c;有些系统对于时间的处理如果不够好的话&…

puppeteer执行js_使用Node.js和Puppeteer与表单和网页进行交互– 1

puppeteer执行jsHi guys! Today lets look at another powerful function of the puppeteer API using Node.js. 嗨&#xff0c;大家好&#xff01; 今天&#xff0c;让我们看看使用Node.js的puppeteer API的另一个强大功能。 In the first part of this section, lets look a…

面试官:不会看SQL执行计划,简历也敢写精通SQL优化?

这是我的第 204 期分享作者 | 程序员内点事来源 | 程序员内点事&#xff08;ID&#xff1a;chengxy-nds&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;昨天中午在食堂&#xff0c;和部门的技术大牛们坐在一桌吃饭&#xff0c;作为一个卑微技…

scrollTop的兼容性小结

2019独角兽企业重金招聘Python工程师标准>>> 在页面上加上了 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 之后&#xff0c;document.body.scrollTop的值…

阿里巴巴为什么让初始化集合时必须指定大小?

这是我的第 205 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;哈喽&#xff0c;亲爱的小伙伴们&#xff0c;技术学磊哥&#xff0c;进步没得说&#xff01;欢迎来到…

ios页面间跳转方式总结

转自&#xff1a;http://www.cnblogs.com/anywherego/p/3542202.html 下面以OldViewController(oldC)的按钮btn点击后跳转到NewViewController(newC)为例说明: 1.Storyboard的segues方式 鼠标点击按钮btn然后按住control键拖拽到newC页面&#xff0c;在弹出的segue页面中选择跳…

啪啪打脸!领导说:try-catch要放在循环体外!

这是我的第 206 期分享作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;哈喽&#xff0c;亲爱的小伙伴们&#xff0c;技术学磊哥&#xff0c;进步没得说&#xff01;欢迎来到…

软件项目与产品的区别与联系_软件产品和软件过程之间的区别和关系

软件项目与产品的区别与联系软件产品和软件过程 (Software product and Software process) Software product and Software process: These two words are the one which is mostly confused with each other. In this article, we are going to explain each of these in deta…

Oracle官方推荐的性能测试工具!简单、精准又直观!

这是我的第 207 期分享作者 | 武培轩来源 | 武培轩&#xff08;ID&#xff1a;wupeixuan404&#xff09;分享 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;磊哥在前面的所有性能优化的文章中都是用了这款性能测试工具——JMH&#xff0c;一直没来得及给大家…