- 场景
比如要实现在1S内,ip是127.0.0.1 的用户对
api/test
接口的请求不能超过 1000 次
- 实现思路
在 redis 队列中,对于同一个 ip,永远保持有1000个以
毫秒为值
的队列,每次请求,取出队列的第一个值,并重新生成一个新的时间值,放在队列的末尾。比较这两个时间,如果间隔超过1S,则表示请求频率超过了设定频率
- 实现步骤
photo_2020-09-07_14-44-16.jpg生成1000个令牌,放入127.0.0.1 的 redis 队列。保持队列中永远有1000个令牌。当有请求过来时,拿掉队列中第一个令牌(比如值是1.1111),并且重新生成一个新的令牌(比如值是1.9999),放在队列的最后一位。
1.9999-1.1111 这个时间在1S内,则表示可以继续请求,如果这个值超过1S,则表示请求频率超出了设定频率
我们使用 php 的 microtime(true)
系统函数,获取当前时间戳和微秒数
1秒=1000000 微秒(μs), 1微秒=1/1000000秒(s);
所以,我们直接使用 microtime(true)*1000
来作为我们的队列值即可
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
$key=\Request::ip(); // 请求的ip 如果我们是对所有请求者做的统一频率限制,则使用固定 key 即可
$redis_len = $redis->lLen($key); // 队列的长度
if ($redis_len <1000){
// 如果长度不够1000 则补齐1000条数据。但理论上是只有第一次数据不够,或者说某个ip好久没有请求,超过了redis key 的设定过期时间。否则key是永远能保持一千条数据的
for ($i=1;$i<=1000-$redis_len){
$redis->rPush($key,microtime(true)*1000);
}
}
// 获取队列的第一个时间
$first_time = $redis->lPop($key); // 从左侧获取第一个值
$now = microtime(true)*1000; // 现在的时间
if(($now-$first_time)>1000){
// 不符合需求,我们还将原来的值放回队列
$redis->lPush($key, $first_time ); // 刚才是从左侧弹出的,我们再从左侧放入,还保证他是第一个值
// 也可以在上面直接获取第一个值,而不是弹出,这样的话,就不用再重新放入这个值了
die('请求频率超过了设定的每秒1000条数据');
}else{
//可以处理本次请求
$redis->rPush($key, $now); // 将现在的时间放入队列
}
-注意
设置频率限制可能有两种方案
一种是,超过频率设定的请求,不做正常的请求处理,如果你一直是每秒1000以上的频率,比如你每秒1500条请求,则程序会在后500条不做处理,之后的请求,已经到了放开频率现在的时间了,会重新处理。上面的代码是按照这种方案处理的
另一种是,超过频率设定的请求,也算是一次请求。这种情况下,如果你一直是每秒1000以上的频率,则你会一直请求失败
不管是否可以请求。正常的队列 lPop 和 rPush 都是要继续的,因为,即使是超过了频率,客户可能还是会以这种频率请求,我们仍然要制止
$start = intval(microtime(true) * 1000);
dump($start); // 1599463467616
usleep(1000 * 100);
$end = intval(microtime(true) * 1000);
dump($end); // 1599463467719
dd($end-$start); // 103
- 注意
程序运行也需要时间,这个时间查是多少,则需要你自己去判断处理的。但是我们的 start 和 end 代码位置很近,理论上不会有太大的时间差。所以这个也可以忽略
网友评论