小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
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.2 pipeline代码实现
代码的实现的逻辑是统计滑动窗口内zset中的行为数量,并且与阈值maxCount直接进行比较就可以判断当前行为是否被允许。这里涉及多个redis操作,因此使用pipeline可以大大提升效率
1 | typescript复制代码package com.lizba.redis.limit; |
测试代码:
1 | java复制代码package com.lizba.redis.limit; |
测试效果: 从测试输出的数据可以看出,起到了限流的效果,从第11次以后的请求操作都是失败的,但是这个和我们允许的5次误差还是比较大的。这个问题的原因是我们测试System.currentTimeMillis()的毫秒可能相同,而且此时value也是System.currentTimeMillis()也相同,会导致zset中元素覆盖! 修改代码测试: 在循环中睡眠1毫秒即可,测试结果符合预期!
1 | bash复制代码 TimeUnit.MILLISECONDS.sleep(1); |
3.3 lua代码实现
我们在项目中使用原子性的lua脚步来实现限流的使用会更多,因此这里也提供一个基于操作zset的lua版本
1 | typescript复制代码package com.lizba.redis.limit; |
测试代码不变,大家可以自行测试,记得还是要考虑我们测试的时候System.currentTimeMillis()相等的问题,不信你输出System.currentTimeMillis()就知道了!多思考问题,技术其实都在心里!
本文转载自: 掘金