我们经常会遇到这样的需求:红包在5分钟内不能领取两次,在1分钟内不能超过两次抽奖,或者某个接口一个IP一天只能访问50次,这种需求就是访问频次限制。
使用PHP的小伙伴应该对APC都不太陌生,APC不但可以用来缓存opcode,而且也可以用做shared memory。利用APC的第二种特性,我们可以用来做类似NGINX的limit_req模块的访问频次限制功能。
访问限制的关键在于需要知道每一次访问的请求时间,然后查看从当前时间往前推的时间段内,有多少次重复的访问。利用APC可以设置ttl的特性,我们可以很简单的做到这点:
apc_store($uniqid, 1, $period); // $period为限制的时间,以“1分钟只能抽两次奖”这个需求为例,$period = 60
其中uniqid变量为一个被限制访问的对象的唯一标识,比如用户的ID,手机设备号,IP等。
上面的代码还有些问题:如果允许在$period时间段内可以访问一次以上,那么$key是不能不变的,否则后一次访问会把前一次的访问设置的$key的过期时间替换掉,也无法判断在$period时间段内,到底有多少次访问。
既然$key不能被替换,那么思路只能是每次访问都要生成不一样的$key,这通过 microtime和 mt_rand两个函数还是很容易做到的:
$key = sprintf('%s.%s%s', %uniqid, $microtime(true), mt_rand(0, 99999));app_store($key, 1, $period);
从PHP函数手册上看,apc_*函数并没有提供可以通过key的前缀来获取所有的key的方法,如果没有这样的方法,上面的思路又被卡死,不过且慢,APC还提供了一个类,叫做APCIterator。解决我们需求的最重要一块拼图就是它了。
从 官方文档来看,这个类构造函数的$search参数,就提供了我们所需要的功能:通过正则的方式来匹配key:
$iter = new APCIterator('user', sprintf('/^%s\./', preg_quote($uniqid)));
从文档里我们可以看到APCIterator里有一个 getTotalCount方法,大家不要被这个方法的名字骗了,从测试的结果来看,这个方法返回的数,居然是包含了已经过期的key的,再加上PHP官方文档对这个方法的描述几乎可以说是毫无用处,这个方法是非常的坑。
这里多说一句:APCIterator还有一个方法叫做 getHitsCount,这里的Hits不是指$search是否能匹配,而是指当前的脚本使用apc_fetch时是否命中。其实方法还是挺好用的,就是文档啥都不写,还得自己试验自己猜,坑死个人。
我们直接将这个Iterator使用foreach遍历一下,可以发现遍历的结果居然是对的(为啥要说居然……),所有有效期内的key都有,不在有效期的key都没有,完全是我们想要的效果。对于“1分钟内每个人只能抽奖两次”这样的需求,我们终于有了解法:
if (iterator_count($iter) >= $count) { // 例子里$count = 2 header('status: 429 Too Many Requests'); die();}
当然,我们还有可以改进的空间,比如说,不同的时间段长度+不同的访问次数应该属于不同的限制策略,所以$key里面应该需要体现时间段长度和访问次数:
$key = sprintf('%s.%s.%s', $uniqid, $period, $count);
代码有些零散,但思路就是这样的。我把代码综合一下变成一个函数,大家更看得明白些吧:
function ratelimit($uniqid, $period, $count){ // ...省去参数检查的代码 $key = sprintf('%s.%s.%s',
已抢66780个
抢已抢38949个
抢已抢21505个
抢已抢2709个
抢已抢1688个
抢已抢19266个
抢