本文大部分内容引自《Redis深度历险:核心原理和应用实践》,感谢作者!!!
Redis简单限流,使用zset来实现
需求:指定用户在指定时间范围内最多请求n次
看了这张图是不是会想象到TCP的流量窗口?
需求分析:
1、限流需求中存在一个滑动的时间窗口,而zset的score值可以用来圈定时间窗口,窗口之外的数据都可以删除
2、zset的value需要是一个唯一的值,只需要保证唯一性即可
3、zset的key应该是用户id和行为的组合userid_action
4、同时还需指定用户行为的过期时间为:时间间隔 + 1 (原因:过期时间应该等于时间窗口的长度,再多宽限1s,看需求而定)
5、缺点是内存有可能占用过多,如果用户60s之内不能请求超过100w次,那么就需要在zset中添加100w个值
使用到的命令
ZADD key score1 member1 [score2 member2] #向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZCARD key #获取有序集合的成员数
ZREMRANGEBYSCORE key min max #移除有序集合中给定的分数区间的所有成员
Java示例代码
public class SimpleRateLimiter {
private Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
/**
* 思想:zadd(用户id+行为)作为key的多个值,使用(zremrangebysecore key 0 当前时间-时间窗口长度 )来除去时间窗口外的记录
* zcard统计key中存在的value的个数即是用户请求的次数
*/
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
String key = String.format("hist:%s:%s", userId, actionKey);
long nowTs = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
pipe.zadd(key, nowTs, "" + nowTs);
pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
Response<Long> count = pipe.zcard(key);
pipe.expire(key, period + 1);
pipe.exec();
pipe.close();
return count.get() <= maxCount;
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
for(int i=0;i<20;i++) {
System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
}
}
}