使用 Redis 如何实现限流功能?
我们可以使用 Redis 中的 ZSet(有序集合)加上滑动时间算法来实现简单的限流。所谓的滑动时间算法指的是以当前时间为截止时间,往前取一定的时间,比如往前取 60s 的时间,在这 60s 之内运行最大的访问数为 100,此时算法的执行逻辑为,先清除 60s 之前的所有请求记录,再计算当前集合内请求数量是否大于设定的最大请求数 100,如果大于则执行限流拒绝策略,否则插入本次请求记录并返回可以正常执行的标识给客户端。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
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 休眠后,正常执行请求
令牌算法
在令牌桶算法中有一个程序以某种恒定的速度生成令牌,并存入令牌桶中,而每个请求需要先获取令牌才能执行,如果没有获取到令牌的请求可以选择等待或者放弃执行.
我们可以使用 Google 开源的 guava 包,很方便的实现令牌桶算法,首先在 pom.xml 添加 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。
更好的限流方案
前面我们将了滑动时间算法的实现代码,但它的缺点是占用内存空间大,并且是非原子操作;而令牌算法为程序级别的单机限流实现方案,有没有更好的分布式限流解决方案呢?
答案是有的,它就是 Redis 4.0 提供的 Redis-Cell 模块,该模块使用的是漏斗算法.
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 为漏斗的速率。```

本文介绍了如何使用Redis的ZSet结合滑动时间窗口算法以及Guava的RateLimiter实现限流功能。通过清除过期请求记录和控制令牌生成速率,达到限制系统并发量的目的。此外,还讨论了Redis-Cell模块提供的漏斗算法作为更优的分布式限流解决方案。

730

被折叠的 条评论
为什么被折叠?



