场景举例
我们日常上网冲浪的时候,常常会遇到这样的情景:
- 多次输错登陆密码,再次尝试登录,页面提示需要输入正确的图形验证码
- 某内容平台限定,每个账号单日可发布的文章数量不得超过规定上限
- 采用短信验证码进行交互的场景,M分钟内请求的验证码次数过多,接下来的N分钟里会被拒绝请求,并提示"用户操作频繁,请稍后再试"
业务逻辑抽离
上述情景虽发生于多个业务场景下,但它们背后的需求本质是相同的,即如何通过控制用户的访问频率来限制恶意流量。这同一类问题在实现上具备相同的逻辑处理流程:
对上述逻辑进一步抽象,可以得到解决问题的关键性元素:
- 限流对象:用户
- 限流操作:高频次请求,多次鉴权未通过等异常请求
- 限流规则:由业务类型决定,如3分钟内请求短信验证码不得超过5次,如果触达上限,接下来的10分钟内拒绝该用户的所有请求,并警示
Redis 限流方案
基于 Redis 的 incr 原子操作,Expires key 过期机制,还有其内存数据库的效率优势,可以相对简单、高效的处理用户访问频率问题。
服务端在接收到用户请求时,首先根据用户发起的操作类型从 Redis 数据库中查询访问记录情况。如果是首次发起请求,则创建新键记录,访问次数记为1,并设置超时时间,以后该用户的每次请求都采用 INCR 命令递增该键的值。否则判断键值是否超出访问频率,如果没有就放行,否则跳转到限流处理逻辑。该键过期后被自动剔除,所以用户又可以正常发起请求。
这是非常容易想到的一种解决方案,但实际操作中会出现如下问题:假设某网站的限流规则是1分钟内的用户请求不能超过100次,这时用户发起了1次请求,1分钟计时开始,在这1分钟的最后1秒,用户发起了99次访问。因为 Redis 的 expires 机制,这个键值对在下一秒就被剔除,而用户在下一分钟的第1秒里又连续发起了99次访问,在系统看来这种走位是合法的,所以并不会加以限制,但却违背了初衷,我们所期望的限流其实是滑动时间窗口计数模式,而不是按照限流计时区间来切割时间。
(java代码待补充)