为什么要限流首先我们来看看为什么在系统架构设计中需要“限流”。旅游景点通常接待量最大,不可能让无限量的游客进入。比如故宫一天只卖8万张门票,超过8万的游客就不能买票进入,因为如果游客超过8万,景区的工作人员可能会忙不过来,景点人满为患也会影响游客的体验和心情,会存在安全隐患;“只卖N张票,这是一种限流手段。”软件架构中的业务流量限制类似。当系统资源不够时,处理大量的请求是不够的。为了保证服务还能正常运行,按照规则,“系统会直接拒绝多余的请求,达到限流的效果”;不知道大家有没有注意到,比如Double11、刚过12点,部分客户的网页或APP会提示下单失败,部分已被限流,常见的限流算法计数方式,顾名思义就是来一个并记录一个。比如我一分钟只能处理1000个请求,那么我们可以设置一个计数器,一个请求incr+1,当一分钟内的数量大于等于1000后,就不要了t处理,伪代码如下$redis=newRedis();$redis->connect('127.0.0.1',6379);$rate_limit=1000;//限制次数$rate_seconds=60;//限制time$redis_key="redis_limit";$count=$redis->get($redis_key);if($count>=$rate_limit){//判断请求次数是否达到上限within60seconds//直接返回,不处理请求return}$redis->incr($redis_key,1);//请求计数$redis->expire($redis,$rate_seconds);//设置过期时间60s//todo业务逻辑处理……。这种统计方式比较简单快捷,但是有一个很大的缺点,就是请求的访问不一定很顺畅。如果0点59分过来1000个请求,1点01分已经是下一个窗口了,1000个请求来了,但实际上三秒内来了2000个请求,已经超过了我们目前的限制。所以不推荐这种方法。滑动窗口算法也拿上面的例子来说,一分钟分为6份,每份10秒;每隔10秒,我们的时间窗口就会向右滑动一格,每一格都有一个独立的计数器。在时间窗口内计算数量可以解决计数器方法中的问题,滑动窗口内的格子越多,限流统计就越准确。详情请参考下图。看伪代码实现更清楚functionapi_limit($scene,$period,$maxCount){$redis=newRedis();$redis->connect('127.0.0.1',6379);$key=sprintf('hist:%s',$scene);//限流场景的唯一标识$now=msectime();//毫秒时间戳,更准确$pipe=$redis->multi(Redis::PIPELINE);//使用管道提高性能$pipe->zadd($key,$now,$now);//Value和score都使用毫秒时间戳$pipe->zremrangebyscore($key,0,$now-$period);//去掉时间窗前的行为记录,剩下时间窗内的$pipe->zcard($key);//获取窗口内的行为数$pipe->expire($key,$period/1000+1);//在过期时间上加一秒$replies=$pipe->exec();return$replies[2]<=$maxCount;//$replies[2]为zcard返回的数字如果zcard结果大于maxCount则不处理结果}for($i=0;$i<20;$i++){//测试是否实现限流代码var_转储(isActionAllowed(“uniq_scene”,60*1000,5));//执行可以发现只有前5次通过}//返回当前毫秒时间戳functionmsectime(){list($msec,$sec)=explode('',microtime());$msectime=(float)sprintf('%.0f',(floatval($msec)+floatval($sec))*1000);return$msectime;}这段代码还是有点复杂,需要读者花费一定的时间。它的总体思路是:当每个行为到达时,维护一个时间窗口。清除时间窗口外的所有记录,只保留窗口内的记录。因为这些连续的Redis操作都是针对同一个key,所以使用pipeline可以显着提高Redis的访问效率。》但是这个方案也有缺点,因为需要记录时间窗口内所有的行为记录,如果量很大,比如限制60s内的操作次数不超过100w次,不适合这样的一个电流限制。因为它消耗了大量的存储空间。后面还有漏桶算法和令牌桶算法。由于他们各自的实现比较复杂,所以准备另开一篇文章单独介绍。
