通过限流,我们可以很好地掌握系统的qps,从而达到保护系统或者接口做事器稳定的目的。
接口限流的常用算法计数器法计数器法是限流算法里最大略也是最随意马虎实现的一种算法。
比如我们规定,对付A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个要求过来的时候,counter就加1,如果counter的值大于100并且该要求与第一个要求的间隔韶光还在1分钟之内,那么解释要求数过多;

如果该要求与第一个要求的间隔韶光大于1分钟,且counter的值还在限流范围内,那么就重置counter。
详细算法的示意图如下:
代码如下
classCounterDemo{private$first_request_time;private$request_count=0;//已要求的次数public$limit=100;//韶光窗口内的最大要求数public$interval=60;//韶光窗口spublicfunction__construct(){$this->first_request_time=time();}publicfunctiongrant(){$now=time();if($now<$this->first_request_time+$this->interval){//韶光窗口内if($this->request_count<$this->limit){$this->request_count++;returntrue;}else{returnfalse;}}else{//超出前一个韶光窗口后,重置第一次要求韶光和要求总次数$this->first_request_time=$now;$this->request_count=1;returntrue;}}}$m=newCounterDemo();$n_success=0;for($i=0;$i<200;$i++){$rt=$m->grant();if($rt){$n_success++;}}echo'成功要求'.$n_success.'次';
计数器算法很大略,但是有个严重的bug:
一个恶意用户在0:59时瞬间发送了100个要求,然后再1:00时又瞬间发送了100个要求,那么这个用户在2秒内发送了200个要求。
上面我们规定1分钟最多处理100个要求, 也便是每秒1.7个要求。用户通过在韶光窗口的重置节点处突发要求, 可以瞬间超过系统的承载能力,导致系统挂起或宕机。
上面的问题,实在是由于我们统计的精度太低造成的。那么如何很好地处理这个问题呢?或者说,如何将临界问题的影响降落呢?我们可以看下面的滑动窗口算法。
上图中,我们把一个韶光窗口(一分钟)分成6份,每份(小格)代表10秒。每过10秒钟我们就把韶光窗口往右滑动一格, 每一个格子都有自己独立的计数器。
比如一个要求在0:35秒到达的时候,就会落在0:30-0:39这个区间,并将此区间的计数器加1。
从上图可以看出, 0:59到达的100个要求会落在0:50-0:59这个灰色的格子中, 而1:00到达的100个要求会落在黄色的格子中。
而在1:00韶光统计时, 窗口会往右移动一格,那么此时的韶光窗口内的要求数量一共是200个,超出了限定的100个,触发了限流,后面的100个要求被抛弃或者等待。
如果我们把窗口韶光划分越多, 比如60格,每格1s, 那么限流统计会更精确。
漏桶算法 (Leaky Bucket)漏桶算法(Leaky Bucket): 平滑网络上的突发流量。使其整流为一个稳定的流量。
有一个固定容量的桶,有水流进来,也有水流出 去。对付流进来的水来说,我们无法估量一共有多少水会流进来,也无法估量水流的速率。但是对付流出去的水来说,这个桶可以固定水流出的速率。当桶满了之后,多余的水将会溢出(多余的要求会被丢弃)。
大略的算法实现:
classLeakyBucketDemo{private$last_req_time;//上一次要求的韶光public$capacity;//桶的容量public$rate;//水漏出的速率(个/秒)public$water;//当前水量(当前累积要求数)publicfunction__construct(){$this->last_req_time=time();$this->capacity=100;$this->rate=20;$this->water=0;}publicfunctiongrant(){$now=time();$water=max(0,$this->water-($now-$this->last_req_time)$this->rate);//先实行漏水,打算剩余水量$this->water=$water;$this->last_req_time=$now;if($water<$this->capacity){//考试测验加水,并且水还未满$this->water+=1;returntrue;}else{//水满,谢绝加水returnfalse;}}}$m=newLeakyBucketDemo();$n_success=0;for($i=0;$i<500;$i++){$rt=$m->grant();if($rt){$n_success++;}if($i>0&&$i%100==0){//每发起100次后停息1secho'已发送',$i,',成功',$n_success,',sleep'.PHP_EOL;sleep(1);}}echo'成功要求'.$n_success.'次';
令牌桶算法比漏桶算法稍显繁芜。首先,我们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的(可用token数为0),token以一个固定的速率r往桶里添补,直到达到桶的容量,多余的令牌将会被丢弃。每当一个要求过来时,就会考试测验从桶里移除一个令牌,如果没有令牌的话,要求无法通过。
代码实现如下:
classTokenBucketDemo{private$last_req_time;//上次要求韶光public$capacity;//桶的容量public$rate;//令牌放入的速率(个/秒)public$tokens;//当前可用令牌的数量publicfunction__construct(){$this->last_req_time=time();$this->capacity=100;$this->rate=20;$this->tokens=100;//开始给100个令牌}publicfunctiongrant(){$now=time();$tokens=min($this->capacity,$this->tokens+($now-$this->last_req_time)$this->rate);//打算桶里可用的令牌数$this->tokens=$tokens;$this->last_req_time=$now;if($this->tokens<1){//若剩余不到1个令牌,则谢绝returnfalse;}else{//还有令牌,领取1个令牌$this->tokens-=1;returntrue;}}}$m=newTokenBucketDemo();$n_success=0;for($i=0;$i<500;$i++){$rt=$m->grant();if($rt){$n_success++;}if($i>0&&$i%100==0){//每发起100次后停息1secho'已发送',$i,',成功',$n_success,',sleep'.PHP_EOL;sleep(1);}}echo'成功要求'.$n_success.'次';
我们可以利用redis的行列步队作为令牌桶容器利用,利用lPush(入队),rPop(出队),实现令牌加入与花费的操作。TokenBucket.php
<?php/ PHP基于Redis利用令牌桶算法实现接口限流,利用redis的行列步队作为令牌桶容器,入队(lPush)出队(rPop)作为令牌的加入与花费操作。publicadd加入令牌publicget获取令牌publicreset重设令牌桶privateconnect创建redis连接/classTokenBucket{//classstartprivate$_config;//redis设定private$_redis;//redis工具private$_queue;//令牌桶private$_max;//最大令牌数/初始化@paramArray$configredis连接设定/publicfunction__construct($config,$queue,$max){$this->_config=$config;$this->_queue=$queue;$this->_max=$max;$this->_redis=$this->connect();}/加入令牌@paramInt$num加入的令牌数量@returnInt加入的数量/publicfunctionadd($num=0){//当前剩余令牌数$curnum=intval($this->_redis->lSize($this->_queue));//最大令牌数$maxnum=intval($this->_max);//打算最大可加入的令牌数量,不能超过最大令牌数$num=$maxnum>=$curnum+$num?$num:$maxnum-$curnum;//加入令牌if($num>0){$token=array_fill(0,$num,1);$this->_redis->lPush($this->_queue,...$token);return$num;}return0;}/获取令牌@returnBoolean/publicfunctionget(){return$this->_redis->rPop($this->_queue)?true:false;}/重设令牌桶,填满令牌/publicfunctionreset(){$this->_redis->delete($this->_queue);$this->add($this->_max);}/创建redis连接@returnLink/privatefunctionconnect(){try{$redis=newRedis();$redis->connect($this->_config['host'],$this->_config['port'],$this->_config['timeout'],$this->_config['reserved'],$this->_config['retry_interval']);if(empty($this->_config['auth'])){$redis->auth($this->_config['auth']);}$redis->select($this->_config['index']);}catch(RedisException$e){thrownewException($e->getMessage());returnfalse;}return$redis;}}?>
令牌的如果与花费
<?php/演示令牌加入与花费/require'TokenBucket.php';//redis连接设定$config=array('host'=>'localhost','port'=>6379,'index'=>0,'auth'=>'','timeout'=>1,'reserved'=>NULL,'retry_interval'=>100,);//令牌桶容器$queue='mycontainer';//最大令牌数$max=5;//创建TrafficShaper工具$tokenBucket=newTokenBucket($config,$queue,$max);//重设令牌桶,填满令牌$tokenBucket->reset();//循环获取令牌,令牌桶内只有5个令牌,因此末了3次获取失落败for($i=0;$i<8;$i++){var_dump($tokenBucket->get());}//加入10个令牌,最大令牌为5,因此只能加入5个$add_num=$tokenBucket->add(10);var_dump($add_num);//循环获取令牌,令牌桶内只有5个令牌,因此末了1次获取失落败for($i=0;$i<6;$i++){var_dump($tokenBucket->get());}?>
领取办法:点赞关注
领取办法:点赞关注小编后私信【资料】获取资料领取办法!