在开发接口服务器的过程中,为了防止客户端对于接口的滥用,保护服务器的资源,
通常来说我们会对于服务器上的各种接口进行调用次数的限制。比如对于某个 用户,他在一个时间段(interval)内,比如 1
分钟,调用服务器接口的次数不能够 大于一个上限(limit),比如说 100
次。如果用户调用接口的次数超过上限的话,就直接拒绝用户的请求,返回错误信息。
服务接口的流量控制策略:分流、降级、限流等。本文讨论下限流策略,虽然降低了服务接口的访问频率和并发量,却换取服务接口和业务应用系统的高可用。
令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
基础版
- 首先设有一个令牌桶,桶内存放令牌,一开始令牌桶内的令牌是满的(桶内令牌的数量可根据服务器情况设定)
- 每次访问从桶内取走一个令牌,当桶内令牌为0,则不允许再访问。
- 每隔一段时间,放入令牌,最多使桶内令牌满额。
class TrafficShaperControllerextends Controller
{
/**
* 令牌桶总数量
*/
private $totleNum = 25;
/**
* 令牌标识(可以根据需要加上关键ID,uid、orderid...)
* @var string
*/
private $quekueName ="TrafficShaper_queue";
/**
* redis缓存类
* @var object
*/
private $redis;
/**
* 初始化方法
*
* @author heyw<1051834593@qq.com>
* @since 2020/12/10
*/
public function _initialize()
{
$this->redis = Redis::getInstance();
}
/**
* 模拟用户消耗令牌
*
* @param int $num
* @author heyw<1051834593@qq.com>
* @since 2020/12/10
*/
public function run($num = 1)
{
// 初始化
$this->reset();
// 模拟1s请求10次
while (1) {
$this->getKey();
sleep(0.1);
}
}
/**
* 获取令牌
*
* @return bool
* @author heyw<1051834593@qq.com>
* @since 2020/12/11
*/
protected function getKey()
{
// 初始化
$redis =$this->redis;
$queueName =$this->quekueName;
// 获取一个令牌,如果没有直接返回
$res =$redis->rPop($queueName);
// 获得令牌,处理业务
var_dump($res ?'get it' :'empty');
return true;
}
/**
* 重置
*
* @author heyw<1051834593@qq.com>
* @since 2020/12/11
*/
protected function reset()
{
$this->redis->delete($this->quekueName);
$this->add(25);
}
/**
* 定时加入令牌桶,1s执行1次
*
* @author heyw<1051834593@qq.com>
* @since 2020/12/10
*/
public function add($stepNum = 5)
{
// 初始化
$redis =$this->redis;
$queueName =$this->quekueName;
// 当前令牌书
$currNum =$redis->lSize($queueName) ?: 0;
$maxNum =$this->totleNum;
$addNum =$maxNum >=$currNum +$stepNum ?$stepNum :$maxNum -$currNum;
if ($addNum == 0) {
return true;
}
// 加入令牌
$token =array_fill(0,$addNum, 1);
$redis->lPush($queueName,$token);
return true;
}
}
进阶版
<?php
namespace Api\Lib;
/**
* 限流控制
*/
class RateLimit
{
private $minNum = 60; //单个用户每分访问数
private $dayNum = 10000; //单个用户每天总的访问量
public function minLimit($uid)
{
$minNumKey = $uid . '_minNum';
$dayNumKey = $uid . '_dayNum';
$resMin = $this->getRedis($minNumKey, $this->minNum, 60);
$resDay = $this->getRedis($minNumKey, $this->minNum, 86400);
if (!$resMin['status'] || !$resDay['status']) {
exit($resMin['msg'] . $resDay['msg']);
}
}
public function getRedis($key, $initNum, $expire)
{
$nowtime = time();
$result = ['status' => true, 'msg' => ''];
$redisObj = $this->di->get('redis');
$redis->watch($key);
$limitVal = $redis->get($key);
if ($limitVal) {
$limitVal = json_decode($limitVal, true);
$newNum = min($initNum, ($limitVal['num'] - 1) + (($initNum / $expire) * ($nowtime - $limitVal['time'])));
if ($newNum > 0) {
$redisVal = json_encode(['num' => $newNum, 'time' => time()]);
} else {
return ['status' => false, 'msg' => '当前时刻令牌消耗完!'];
}
} else {
$redisVal = json_encode(['num' => $initNum, 'time' => time()]);
}
$redis->multi();
$redis->set($key, $redisVal);
$rob_result = $redis->exec();
if (!$rob_result) {
$result = ['status' => false, 'msg' => '访问频次过多!'];
}
return $result;
}
}
1:首先定义规则
单个用户每分钟访问次数
(dayNum),接口总的访问次数等不同的规则。
2:计算速率
该代码示例以秒为最小的时间单位,速率=访问次数/时间($initNum / $expire)
3:每次访问后补充的令牌个数计算方式
获取上次访问的时间即上次存入令牌的时间,计算当前时刻与上次访问的时间差乘以速率就是此次需要补充的令牌个数,注意补充令牌后总的令牌个数不能大于初始化的令牌个数,以补充数和初始化数的最小值为准。
4:程序流程
第一次访问时初始化令牌个数($minNum),存入Redis同时将当前的时间戳存入以便计算下次需要补充的令牌个数。第二次访问时获取剩余的令牌个数,并添加本次应该补充的令牌个数,补充后如何令牌数>0则当前访问是有效的可以访问,否则令牌使用完毕不可访问。先补充令牌再判断令牌是否>0的原因是由于还有速率这个概念即如果上次剩余的令牌为0但是本次应该补充的令牌>1那么本次依然可以访问。