为了避免恶意行为导致的过载,通常采用令牌桶和漏桶进行限流。下面简单介绍一下令牌桶和漏桶的实现。
对于每个独立的访客,redis 会为他建立两个 key,一个 key 保存了剩余令牌的数量,另外一个 key 保存了最近一次访问的时间戳。其中,最近一次访问时间戳在新访问到来时候用于计算时间间隔,从而计算在此时间间隔内应该向令牌桶中添加或减少多少令牌,进而获得当前令牌桶的剩余令牌数。
1. 令牌桶
(如果有足够的令牌,则发放(返回true),如果不够,
计算需要等待时间,
得出的等待时间大于系统设置的最长等待时间,则不发放(返回false)
得出的等待时间小于系统设置的最长等待时间,则sleep后,继续计算,
有足够令牌返回true,否则返回false
2. 漏桶
先执行漏掉令牌,计算剩余可用令牌数量,
如果当前需要的令牌数量+可用的数量,之和不大于桶的容量,则请求可以被处理返回true
否则请求丢弃返回false
class BucketLimiter
{
protected $maxTokens;
protected $availableTokens = 100;
protected $startMilli;
protected $rate;
protected $maxWaitTime = 100;
protected $intervalTokenMilli;
protected $timeout = 60;
public function __construct($maxTokens, $rate)
{
$this->maxTokens = $maxTokens;
$this->rate = $rate;
$this->intervalTokenMilli = 1 / $rate;
if ($this->di == null) {
$this->di = \Phalcon\DI::getDefault();
}
}
/**
* 令牌桶获取令牌
* @DateTime 2017-10-17T09:37:01+0800
* @param [type] $requierdToken [description]
* @return [type] [description]
*/
public function getTBToken($requierdToken, $id)
{
$now = $this->getNowTime();
$num = $this->di['redis']->get('tbkt:num:' . $id);
$startTime = $this->di['redis']->get('tbkt:start:' . $id);
if ($startTime) {
$this->startMilli = $startTime;
$this->availableTokens = $num;
$lock = $this->di['redis']->lock();
if ($lock) {
// 先添加令牌
$this->availableTokens = intval(min($this->maxTokens, $this->availableTokens + ($now - $this->startMilli) * $this->rate));
$this->di['redis']->unlock();
}
}
if ($requierdToken <= $this->availableTokens) {
$this->availableTokens -= $requierdToken;
$this->di['redis']->set('tbkt:num:' . $id, $this->availableTokens, '', $this->timeout);
$this->di['redis']->set('tbkt:start:' . $id, $now, '', $this->timeout);
return true;
}
$tokenPermitted = $this->availableTokens;
$needNewToken = $requierdToken - $tokenPermitted;
$waitTime = $needNewToken * $this->intervalTokenMilli;
if ($waitTime < $this->maxWaitTime) {
usleep($waitTime * 1000);
$this->startMilli = $now;
$now = $this->getNowTime();
$this->availableTokens = min($this->maxTokens, $this->availableTokens + ($now - $this->startMilli) * $this->rate);
if ($requierdToken <= $this->availableTokens) {
$this->availableTokens -= $requierdToken;
$this->di['redis']->set('tbkt:num:' . $id, $this->availableTokens, '', $this->timeout);
$this->di['redis']->set('tbkt:start:' . $id, $now, '', $this->timeout);
return true;
}
}
return false;
}
/**
* 漏桶
* @DateTime 2017-10-17T09:37:36+0800
* @param [type] $requierdToken [description]
* @return [type] [description]
*/
public function getLBToken($requierdToken, $id)
{
$now = getNowTime();
$num = $this->di['redis']->get('lbkt:num:' . $id);
$startTime = $this->di['redis']->get('lbkt:start:' . $id);
if ($startTime) {
$this->availableTokens = $num;
$this->startMilli = $startTime;
// 先执行漏令牌,计算剩余令牌量
$this->availableTokens = max(0, $this->availableTokens - ($now - $this->startMilli) * $this->rate);
}
if (($this->availableTokens + $requierdToken) < $this->maxTokens) {
// 尝试加令牌,并且令牌还未满
$this->availableTokens += $requierdToken;
$this->di['redis']->set('lbkt:num:' . $id, $this->availableTokens, '', $this->timeout);
$this->di['redis']->set('lbkt:start:' . $id, $now, '', $this->timeout);
return true;
} else {
// 桶满,拒绝加令牌
return false;
}
}
private function getNowTime()
{
list($s1, $s2) = explode(' ', microtime());
return (float) sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000);
}
}