现在系统中由于各种需要,经常遇到一种场景:需要限定每个IP地址每分钟最大访问次数类似的需求。下面是使用Redis实现范文频率限制的一种方式。

场景一:要限制每分钟每个用户最多只能访问100个页面。

思路:
对每个用户使用一个名为“rate.limiting:用户IP”的字符串类型键;
每次用户访问,使用INCR命令递增该键的键值
如果递增后的值是1(第一次访问页面),则同时还要设置该键的生存时间为1分钟
这样每次访问页面时都读取该键的键值,如果超过了100就表明该用户的访问频率超过了限制
PS:该键每分钟会自动被删除,所以下一分钟用户的访问次数又会重新计算,这样就达到了限制访问频率的目的。
上述流程的伪代码如下:
Redis伪代码

 $isKeyExists = EXISTS rate.limiting:$IP  
 if $isKeyExists is 1  
    $times = INCR rate.limiting:$IP  
     if $times > 100  
         print 访问频率超过了限制,请稍后再试  
         exit  
  else  
     INCR rate.limiting:$IP  
     EXPIRE $keyName, 60  

上面的这段代码存在一个不太明显的问题:加入程序执行完倒数第二行后,因为某种原因突然退出了,没能够为该键值设置生存时间,那么该键会永久存在,导致使用对应IP的用户最多只能访问系统100次,除非管理员手动删除该键。这是一个很严重的问题,但是可以结合Redis的事务功能解决该问题,修改后的伪代码如下:
Redis伪代码

$isKeyExists = EXISTS rate.limiting:$IP  
  if $isKeyExists is 1  
      $times = INCR rate.limiting:$IP  
      if $times > 100   
  elsee  
      EXPIRE $keyName, 60  
      EXEC

访问频率限制到此基本上已经实现,但是仍然有细节地方可以改进。

场景二:任意一分钟内每个用户最多只能访问100个页面。

一个用户在1分钟的第1秒访问了1次系统,在同一分钟的最后1秒访问了99次;又在下一分钟的第一秒访问了100次系统,这种情况用户实际上在2秒内访问了199次系统,这与每个用户每分钟只能访问100次的限制的差距较大。

尽管这种情况比较极端,但是在一些场合中还是需要粒度更小的控制方案。

问题解决思路:如果要精确的保证每分钟最多访问100次,需要记录下每次访问的时间。因此对每个用户,我们使用一个列表类型的键来记录他最近100次访问时间,一旦键中的元素超过100个,就判断时间最早的元素距离现在的时间是否小于1分钟。如果是则表示最近1分钟内的访问次数超过了100次;如果不是就讲现在的时间加入到列表中,同时把最早的元素删除。

上述流程的伪代码如下:
Redis伪代码

  $listLength = LLEN rate.limiting:$IP  
  if $listLength < 100  
      LPUSH rate.limiting:$IP, now()  
  else  
      $time = LINDEX rate.limiting:$IP, -1  
      if now() - $time < 60  
          print 访问频率超过了限制,请稍后再试  
      else  
          LPUSH rate.limiting:$IP, now()  
          LTRIM rate.limiting:$IP, 0, 99  
    ```
上述代码中用now()函数获得当前的Unix时间。由于需要记录每次访问的时间,所以当要限制“单位时间最多访问N次” 时,如果N的数值越大,此方法占用的存储空间就越多,实际使用时还需要开发者自己去权衡。除此之外,该方法也会出现竞态条件,使用时请注意。