Redis实践 利用Redis实现简单限流
利用Redis来限流,可以限定用户的某个行为在指定的时间里只能允许发生N次。
场景: 某个用户在一秒内只能回复5次,那么利用Redis如何实现呢。
思路:这个限流需求中存在一个滑动时间窗口,我们可以联想到zset数据结构的score值,我们可以通过score来圈出这个时间窗口来。而且我们只需要维护这个时间窗口,窗口之外的数据都可以砍掉。那这个zset 的value填什么比较合适呢?它只需要保证唯一性即可,用 uuid 会比较浪费空间,改用毫秒时间戳比较好。
图如下:
now_ts是当前的毫秒时间戳,我们只需要维护[now_s-period,now_s]这段时间里用户的操作数即可。
具体代码:
public class SimpleRateLimiter {
private final Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
String key=String.format("hist6:%s:%s",userId,actionKey);
long nowTs=System.currentTimeMillis();
//毫秒时间戳
Pipeline pipeline=jedis.pipelined();
pipeline.multi();//用了multi,也就是事务,能保证一系列指令的原子顺序执行
//value和score都使用毫秒时间戳
pipeline.zadd(key,nowTs,nowTs+"");
//移除时间窗口之前的行为记录,剩下的都是时间窗口内的
pipeline.zremrangeByScore(key,0,nowTs-period*1000);
//获得[nowTs-period*1000,nowTs]的key数量
Response<Long> count=pipeline.zcard(key);
//每次设置都能保持更新key的过期时间
pipeline.expire(key,period);
pipeline.exec();
pipeline.close();
return count.get()<=maxCount;
}
public static void main(String[] args) throws IOException, InterruptedException {
Jedis jedis=new Jedis("localhost",6379);
jedis.auth("iostream");
SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
for (int i = 0; i < 20; i++) {
//每个用户在1秒内最多能做五次动作
System.out.println(limiter.isActionAllow("viscu","reply",1,5));
}
}
}
由于毫秒时间戳的精度问题,1ms内可能有执行好几次操作,有zset的去重操作,所以会看到true出现了超过5次,说明还不够精确。
我下面用了Thread.sleep(1)来模拟了不同操作的间的时间间隔 可是这种方法并不提倡
public class SimpleRateLimiter {
private final Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
String key=String.format("hist6:%s:%s",userId,actionKey);
long nowTs=System.currentTimeMillis();
//毫秒时间戳
Pipeline pipeline=jedis.pipelined();
pipeline.multi();
//value和score都使用毫秒时间戳
pipeline.zadd(key,nowTs,nowTs+"");
//移除时间窗口之前的行为记录,剩下的都是时间窗口内的
pipeline.zremrangeByScore(key,0,nowTs-period*1000);
//获得[nowTs-period*1000,nowTs]的key数量
Response<Long> count=pipeline.zcard(key);
//每次设置都能更新key的过期时间
pipeline.expire(key,period);
pipeline.exec();
pipeline.close();
return count.get()<=maxCount;
}
public static void main(String[] args) throws IOException, InterruptedException {
Jedis jedis=new Jedis("localhost",6379);
jedis.auth("iostream");
SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
while (true){
Thread.sleep(1000); //这里模拟每次经过1s限流之后 "viscu"这个用户就可以重新进行"reply"行为的操作。
for (int i = 0; i < 20; i++) {
//模拟每个动作之间的间隔时间为1ms 具体看情况8 这里只是简单模拟一下。
Thread.sleep(1);
//这样就确保了该用户在1秒内最多能做五次动作
System.out.println(limiter.isActionAllow("viscu","reply",1,5));
}
}
}
}
或者我们可以使用纳秒这种更加精确的数或者加上随机数:
public class SimpleRateLimiter {
private final Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
String key=String.format("hist6:%s:%s",userId,actionKey);
long nowTs=System.nanoTime();
//纳秒时间戳
Pipeline pipeline=jedis.pipelined();
pipeline.multi();
//value和score都使用纳秒时间戳
pipeline.zadd(key,nowTs,nowTs+"");
pipeline.zremrangeByScore(key,0,nowTs-period*1000*1000*1000);
Response<Long> count=pipeline.zcard(key);
pipeline.expire(key,period);
pipeline.exec();
pipeline.close();
return count.get()<=maxCount;
}
public static void main(String[] args) throws IOException, InterruptedException {
Jedis jedis=new Jedis("localhost",6379);
jedis.auth("iostream");
SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
for (int i = 0; i < 20; i++) {
System.out.println(limiter.isActionAllow("viscu","reply",1,5));
}
}
}
- 参考: Redis 深度历险:核心原理与应用实践
题目和代码都是来自这篇文章,只是加上了一些自己的思考。