利用 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.时间窗口算法所谓的滑动时间算法指的是以当前时间为截止时间,往前取一定的时间,比如往前取 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.漏桶算法漏桶算法的灵感源于漏斗,如下图所示: