Redis滑动窗口实现访问频率控制

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

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

思路:

  1. 对每个用户使用一个名为“rate.limiting:用户IP”的字符串类型键;
  2. 每次用户访问,使用INCR命令递增该键的键值
  3. 如果递增后的值是1(第一次访问页面),则同时还要设置该键的生存时间为1分钟
  4. 这样每次访问页面时都读取该键的键值,如果超过了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的数值越大,此方法占用的存储空间就越多,实际使用时还需要开发者自己去权衡。除此之外,该方法也会出现竞态条件,使用时请注意。

转载于:https://my.oschina.net/MyoldTime/blog/3094899

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于Redis滑动窗口流量控制算法可以通过Redis的ZSET数据结构实现。具体实现步骤如下: 1. 定义一个时间窗口,例如1分钟,将这个时间窗口分成多个小时间片,例如每秒一个时间片。 2. 每个时间片对应一个ZSET,ZSET中的元素为请求的时间戳,分值为1。 3. 当有请求到达时,将请求的时间戳作为元素插入到当前时间片对应的ZSET中。 4. 统计整个时间窗口内的请求总数,可以通过遍历所有时间片对应的ZSET并累加元素数量得到。 5. 如果请求总数超过了限流阈值,则拒绝本次请求。 6. 为了避免ZSET无限增长,需要定期清理过期的时间片。 下面是一个PHP实现的示例代码: ```php class SlidingWindow { private $redis; private $windowSize; private $limit; public function __construct($redis, $windowSize, $limit) { $this->redis = $redis; $this->windowSize = $windowSize; $this->limit = $limit; } public function hit() { $now = microtime(true); $key = $this->getKey($now); $this->redis->zAdd($key, $now, $now); $this->redis->expire($key, $this->windowSize); $count = $this->getCount(); if ($count > $this->limit) { throw new Exception('Rate limit exceeded'); } } private function getKey($now) { $start = floor($now / $this->windowSize) * $this->windowSize; $end = $start + $this->windowSize - 1; return "sliding_window:{$start}:{$end}"; } private function getCount() { $now = microtime(true); $start = floor($now / $this->windowSize) * $this->windowSize; $end = $start + $this->windowSize - 1; $keys = []; for ($i = $start; $i <= $end; $i += 1) { $keys[] = $this->getKey($i); } $this->redis->zUnion($key, $keys); $this->redis->expire($key, $this->windowSize); return $this->redis->zCard($key); } } // 使用示例 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $windowSize = 60; // 时间窗口为60秒 $limit = 100; // 限流阈值为100个请求 $slidingWindow = new SlidingWindow($redis, $windowSize, $limit); try { $slidingWindow->hit(); echo 'Request accepted'; } catch (Exception $e) { echo 'Request rejected: ' . $e->getMessage(); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值