问题描述
限流的目的主要是控制用户行为,避免垃圾请求,比如在一些社区论坛中,用户的发帖,回复、点赞等行为都要严格受控。一般要严格限定某行为在规定时间内被运行的次数,超过了次数就是非法行为。对非法行为做相应的处理。
一般在应用场景中,会限制用户的某个行为在规定的时间内只能允许发生N次。
解决方案
使用滑动时间窗口(定宽),只需要保留这个时间窗口,窗口之外的数据都可以砍掉。zset中的value没有什么实际意义,只需要保证唯一性即可。
如果是冷用户滑动时间窗口内的行为是空记录,那么这个zset就可以从内存中移除,不再占用空间。
代码实现
SimpleRateLimiter.java
package com.ryz2593.happy.study.redis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
import java.io.IOException;
/**
* 使用Redis实现简单限流策略
* 使用滑动时间窗口(定宽),只需要保留这个时间窗口,窗口之外的数据都可以砍掉。
* 如果是冷用户滑动时间窗口内的行为是空记录,那么这个zset就可以从内存中移除,不再占用空间。
* @author ryz2593
*/
public class SimpleRateLimiter {
private Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
/**
* @param userId
* @param actionKey
* @param period
* @param maxCount
* @return 当前的行为是否被允许
* @throws IOException
*/
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) throws IOException {
String key = String.format("hist:%s:%s", userId, actionKey);
long nowTs = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
//value 和 score 都使用毫秒时间戳
pipe.zadd(key, nowTs, "" + nowTs);
//移除时间窗口之前的行为记录,剩下的都是时间窗口内的
pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
//获取窗口内的行为数量
Response<Long> count = pipe.zcard(key);
//设置zset过期时间,避免冷用户持续占用内存
//过期时间应该等于时间窗口的长度,再多宽限1s
pipe.expire(key, period + 1);
pipe.exec();
pipe.close();
//比较数量是否超过允许的最大值
return count.get() <= maxCount;
}
public static void main(String[] args) throws IOException {
Jedis jedis = new Jedis("localhost");
SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
for (int i = 0; i < 20; i++) {
System.out.println(limiter.isActionAllowed("ryz2593", "click", 60, 5));
}
}
}
运行结果
true
true
true
true
true
true
false
false
false
false
false
false
false
false
false
false
false
false
false
false
整体思路
每一个行为到来时,都维护一次时间窗口。将时间窗口之外的记录全部清理掉,只保留窗口内的记录。 zset集合中只有score值非常重要,value值没有特别的意义,只需要保证唯一的就可以。
因为这几个连续的Redis操作都是针对同一个key的,使用pipeline可以显著提升Redis的存取效率。
但这种方案也有缺点,因为要记录时间窗口内所有的行为记录,如果这个量很大,比如“限定1min内操作不超过10万次”之类,它是不适合做这样的限流的,因为会消耗大量的存储空间。