架构师如何讲解Redis限流——滑动窗口限流

发布网友 发布时间:2024-10-24 02:41

我来回答

1个回答

热心网友 时间:2024-11-20 12:57

1、需求

限定用户的某个行为在指定时间T内,只允许发生N次。假设T为1秒钟,N为1000次。

2、常见的错误设计

程序员设计了一个在每分钟内只允许访问1000次的限流方案,如下图01:00s-02:00s之间只允许访问1000次,这种设计最大的问题在于,请求可能在01:59s-02:00s之间被请求1000次,02:00s-02:01s之间被请求了1000次,这种情况下01:59s-02:01s间隔0.02s之间被请求2000次,很显然这种设计是错误的。

3、滑动窗口算法3.1解决方案

指定时间T内,只允许发生N次。我们可以将这个指定时间T,看成一个滑动时间窗口(定宽)。我们采用Redis的zset基本数据类型的score来圈出这个滑动时间窗口。在实际操作zset的过程中,我们只需要保留在这个滑动时间窗口以内的数据,其他的数据不处理即可。

每个用户的行为采用一个zset存储,score为毫秒时间戳,value也使用毫秒时间戳(比UUID更加节省内存)

只保留滑动窗口时间内的行为记录,如果zset为空,则移除zset,不再占用内存(节省内存)

3.2pipeline代码实现

代码的实现的逻辑是统计滑动窗口内zset中的行为数量,并且与阈值maxCount直接进行比较就可以判断当前行为是否被允许。这里涉及多个Redis操作,因此使用pipeline可以大大提升效率

packagecom.lizba.redis.limit;importredis.clients.jedis.Jedis;importredis.clients.jedis.Pipeline;importredis.clients.jedis.Response;/***<p>*通过zset实现滑动窗口算法限流*</p>**@Author:Liziba*@Date:2021/9/618:11*/publicclassSimpleSlidingWindowByZSet{privateJedisjedis;publicSimpleSlidingWindowByZSet(Jedisjedis){this.jedis=jedis;}/***判断行为是否被允许**@paramuserId用户id*@paramactionKey行为key*@paramperiod限流周期*@parammaxCount最大请求次数(滑动窗口大小)*@return*/publicbooleanisActionAllowed(StringuserId,StringactionKey,intperiod,intmaxCount){Stringkey=this.key(userId,actionKey);longts=System.currentTimeMillis();Pipelinepipe=jedis.pipelined();pipe.multi();pipe.zadd(key,ts,String.valueOf(ts));//移除滑动窗口之外的数据pipe.zremrangeByScore(key,0,ts-(period*1000));Response<Long>count=pipe.zcard(key);//设置行为的过期时间,如果数据为冷数据,zset将会删除以此节省内存空间pipe.expire(key,period);pipe.exec();pipe.close();returncount.get()<=maxCount;}/***限流key**@paramuserId*@paramactionKey*@return*/publicStringkey(StringuserId,StringactionKey){returnString.format("limit:%s:%s",userId,actionKey);}}

测试代码:

packagecom.lizba.redis.limit;importredis.clients.jedis.Jedis;/****@Author:Liziba*@Date:2021/9/620:10*/publicclassTestSimpleSlidingWindowByZSet{publicstaticvoidmain(String[]args){Jedisjedis=newJedis("192.168.211.108",6379);SimpleSlidingWindowByZSetslidingWindow=newSimpleSlidingWindowByZSet(jedis);for(inti=1;i<=15;i++){booleanactionAllowed=slidingWindow.isActionAllowed("liziba","view",60,5);System.out.println("第"+i+"次操作"+(actionAllowed?"成功":"失败"));}jedis.close();}}

测试效果:从测试输出的数据可以看出,起到了限流的效果,从第11次以后的请求操作都是失败的,但是这个和我们允许的5次误差还是比较大的。这个问题的原因是我们测试System.currentTimeMillis()的毫秒可能相同,而且此时value也是System.currentTimeMillis()也相同,会导致zset中元素覆盖!

修改代码测试:在循环中睡眠1毫秒即可,测试结果符合预期!

TimeUnit.MILLISECONDS.sleep(1);3.3lua代码实现

我们在项目中使用原子性的lua脚步来实现限流的使用会更多,因此这里也提供一个基于操作zset的lua版本

packagecom.lizba.redis.limit;importcom.google.common.collect.ImmutableList;importredis.clients.jedis.Jedis;importredis.clients.jedis.Pipeline;importredis.clients.jedis.Response;/***<p>*通过zset实现滑动窗口算法限流*</p>**@Author:Liziba*@Date:2021/9/618:11*/publicclassSimpleSlidingWindowByZSet{privateJedisjedis;publicSimpleSlidingWindowByZSet(Jedisjedis){this.jedis=jedis;}/***lua脚本限流**@paramuserId*@paramactionKey*@paramperiod*@parammaxCount*@return*/publicbooleanisActionAllowedByLua(StringuserId,StringactionKey,intperiod,intmaxCount){StringluaScript=this.buildLuaScript();Stringkey=key(userId,actionKey);longts=System.currentTimeMillis();System.out.println(ts);ImmutableList<String>keys=ImmutableList.of(key);ImmutableList<String>args=ImmutableList.of(String.valueOf(ts),String.valueOf((ts-period*1000)),String.valueOf(period));Numbercount=(Number)jedis.eval(luaScript,keys,args);returncount!=null&&count.intValue()<=maxCount;}/***限流key**@paramuserId*@paramactionKey*@return*/privateStringkey(StringuserId,StringactionKey){returnString.format("limit:%s:%s",userId,actionKey);}/***针对某个key使用lua脚本限流**@return*/privateStringbuildLuaScript(){return"redis.call('ZADD',KEYS[1],tonumber(ARGV[1]),ARGV[1])"+"\nlocalc"+"\nc=redis.call('ZCARD',KEYS[1])"+"\nredis.call('ZREMRANGEBYSCORE',KEYS[1],0,tonumber(ARGV[2]))"+"\nredis.call('EXPIRE',KEYS[1],tonumber(ARGV[3]))"+"\nreturnc;";}}

测试代码不变,大家可以自行测试,记得还是要考虑我们测试的时候System.currentTimeMillis()相等的问题,不信你输出System.currentTimeMillis()就知道了!多思考问题,技术其实都在心里!

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com