php 基於redis使用令牌桶演算法實現流量控制
本文介紹php基於redis,使用令牌桶演算法,實現訪問流量的控制,提供完整演算法說明及演示例項,方便大家學習使用。
每當國內長假期或重要節日時,國內的景區或地鐵都會人山人海,導致負載過大,部分則會採用限流措施,限制進入的人數,當區內人數降低到一定值,再允許進入。
例如:
區內最大允許人數為 M
區內當前人數為 N
每進入一個人,N+1,當N = M時,則不允許進入
每離開一個人,N-1,當N < M時,可允許進入
系統在執行過程中,如遇上某些活動,訪問的人數會在一瞬間內爆增,導致伺服器瞬間壓力飆升,使系統超負荷工作。
當然我們可以增加伺服器去分擔壓力,首先增加伺服器也需要一定的時間去配置,而且因為某一個活動而增加伺服器,活動結束後這些伺服器資源就浪費了。
因此我們可以根據業務型別,先使用限流的方式去減輕伺服器壓力。
與景區限流不同,系統的訪問到結束的時間非常短,因此我們只需要知道每個訪問持續的平均時間,設定最多同時訪問的人數即可。
令牌桶演算法
1.首先設有一個令牌桶,桶記憶體放令牌,一開始令牌桶內的令牌是滿的(桶內令牌的數量可根據伺服器情況設定)。
2.每次訪問從桶內取走一個令牌,當桶內令牌為0,則不允許再訪問。
3.每隔一段時間,再放入令牌,最多使桶內令牌滿額。(可以根據實際情況,每隔一段時間放入若干個令牌,或直接補滿令牌桶)
我們可以使用redis的佇列作為令牌桶容器使用,使用lPush(入隊),rPop(出隊),實現令牌加入與消耗的操作。
TrafficShaper.class.php
<?php
/**
* PHP基於Redis使用令牌桶演算法實現流量控制
* Date: 2018-02-23
* Author: fdipzone
* Version: 1.0
*
* Descripton:
* php基於Redis使用令牌桶演算法實現流量控制,使用redis的佇列作為令牌桶容器,入隊(lPush)出隊(rPop)作為令牌的加入與消耗操作。
*
* Func:
* public add 加入令牌
* public get 獲取令牌
* public reset 重設令牌桶
* private connect 建立redis連線
*/
class TrafficShaper{ // class start
private $_config; // redis設定
private $_redis; // redis物件
private $_queue; // 令牌桶
private $_max; // 最大令牌數
/**
* 初始化
* @param Array $config redis連線設定
*/
public function __construct($config, $queue, $max){
$this->_config = $config;
$this->_queue = $queue;
$this->_max = $max;
$this->_redis = $this->connect();
}
/**
* 加入令牌
* @param Int $num 加入的令牌數量
* @return Int 加入的數量
*/
public function add($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;
}
return 0;
}
/**
* 獲取令牌
* @return Boolean
*/
public function get(){
return $this->_redis->rPop($this->_queue)? true : false;
}
/**
* 重設令牌桶,填滿令牌
*/
public function reset(){
$this->_redis->delete($this->_queue);
$this->add($this->_max);
}
/**
* 建立redis連線
* @return Link
*/
private function connect(){
try{
$redis = new Redis();
$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){
throw new Exception($e->getMessage());
return false;
}
return $redis;
}
} // class end
?>
demo:
<?php
/**
* 演示令牌加入與消耗
*/
require 'TrafficShaper.class.php';
// redis連線設定
$config = array(
'host' => 'localhost',
'port' => 6379,
'index' => 0,
'auth' => '',
'timeout' => 1,
'reserved' => NULL,
'retry_interval' => 100,
);
// 令牌桶容器
$queue = 'mycontainer';
// 最大令牌數
$max = 5;
// 建立TrafficShaper物件
$oTrafficShaper = new TrafficShaper($config, $queue, $max);
// 重設令牌桶,填滿令牌
$oTrafficShaper->reset();
// 迴圈獲取令牌,令牌桶內只有5個令牌,因此最後3次獲取失敗
for($i=0; $i<8; $i++){
var_dump($oTrafficShaper->get());
}
// 加入10個令牌,最大令牌為5,因此只能加入5個
$add_num = $oTrafficShaper->add(10);
var_dump($add_num);
// 迴圈獲取令牌,令牌桶內只有5個令牌,因此最後1次獲取失敗
for($i=0; $i<6; $i++){
var_dump($oTrafficShaper->get());
}
?>
輸出:
boolean true
boolean true
boolean true
boolean true
boolean true
boolean false
boolean false
boolean false
int 5
boolean true
boolean true
boolean true
boolean true
boolean true
boolean false
定期加入令牌演算法
定期加入令牌,我們可以使用crontab實現,每分鐘呼叫add方法加入若干令牌。crontab的使用可以參考:《Linux crontab定時執行任務 命令格式與詳細例子》
crontab最小的執行間隔為1分鐘,如果令牌桶內的令牌在前幾秒就已經被消耗完,那麼剩下的幾十秒時間內,都獲取不到令牌,導致使用者等待時間較長。
我們可以優化加入令牌的演算法,改為一分鐘內每若干秒加入若干令牌,這樣可以保證一分鐘內每段時間都有機會能獲取到令牌。
crontab呼叫的加入令牌程式如下,每秒自動加入3個令牌。
<?php
/**
* 定時任務加入令牌
*/
require 'TrafficShaper.class.php';
// redis連線設定
$config = array(
'host' => 'localhost',
'port' => 6379,
'index' => 0,
'auth' => '',
'timeout' => 1,
'reserved' => NULL,
'retry_interval' => 100,
);
// 令牌桶容器
$queue = 'mycontainer';
// 最大令牌數
$max = 10;
// 每次時間間隔加入的令牌數
$token_num = 3;
// 時間間隔,最好是能被60整除的數,保證覆蓋每一分鐘內所有的時間
$time_step = 1;
// 執行次數
$exec_num = (int)(60/$time_step);
// 建立TrafficShaper物件
$oTrafficShaper = new TrafficShaper($config, $queue, $max);
for($i=0; $i<$exec_num; $i++){
$add_num = $oTrafficShaper->add($token_num);
echo '['.date('Y-m-d H:i:s').'] add token num:'.$add_num.PHP_EOL;
sleep($time_step);
}
?>
模擬消耗程式如下,每秒消耗2-8個令牌。
<?php
/**
* 模擬使用者訪問消耗令牌,每段時間間隔消耗若干令牌
*/
require 'TrafficShaper.class.php';
// redis連線設定
$config = array(
'host' => 'localhost',
'port' => 6379,
'index' => 0,
'auth' => '',
'timeout' => 1,
'reserved' => NULL,
'retry_interval' => 100,
);
// 令牌桶容器
$queue = 'mycontainer';
// 最大令牌數
$max = 10;
// 每次時間間隔隨機消耗的令牌數量範圍
$consume_token_range = array(2, 8);
// 時間間隔
$time_step = 1;
// 建立TrafficShaper物件
$oTrafficShaper = new TrafficShaper($config, $queue, $max);
// 重設令牌桶,填滿令牌
$oTrafficShaper->reset();
// 執行令牌消耗
while(true){
$consume_num = mt_rand($consume_token_range[0], $consume_token_range[1]);
for($i=0; $i<$consume_num; $i++){
$status = $oTrafficShaper->get();
echo '['.date('Y-m-d H:i:s').'] consume token:'.($status? 'true' : 'false').PHP_EOL;
}
sleep($time_step);
}
?>
演示
設定定時任務,每分鐘執行一次
* * * * * php /程式的路徑/cron_add.php >> /tmp/cron_add.log
執行模擬消耗
php consume_demo.php
執行結果:
[2018-02-23 11:42:57] consume token:true
[2018-02-23 11:42:57] consume token:true
[2018-02-23 11:42:57] consume token:true
[2018-02-23 11:42:57] consume token:true
[2018-02-23 11:42:57] consume token:true
[2018-02-23 11:42:57] consume token:true
[2018-02-23 11:42:57] consume token:true
[2018-02-23 11:42:58] consume token:true
[2018-02-23 11:42:58] consume token:true
[2018-02-23 11:42:58] consume token:true
[2018-02-23 11:42:58] consume token:true
[2018-02-23 11:42:58] consume token:true
[2018-02-23 11:42:58] consume token:true
[2018-02-23 11:42:58] consume token:false
[2018-02-23 11:42:59] consume token:true
[2018-02-23 11:42:59] consume token:true
[2018-02-23 11:42:59] consume token:true
[2018-02-23 11:42:59] consume token:false
[2018-02-23 11:42:59] consume token:false
[2018-02-23 11:42:59] consume token:false
[2018-02-23 11:42:59] consume token:false
[2018-02-23 11:43:00] consume token:true
[2018-02-23 11:43:00] consume token:true
[2018-02-23 11:43:00] consume token:true
[2018-02-23 11:43:00] consume token:false
[2018-02-23 11:43:00] consume token:false
因令牌桶一開始是滿的(最大令牌數10),所以之前的10次都能獲取到令牌,10次之後則會根據消耗的令牌大於加入令牌數時,限制訪問。
原始碼下載地址:點選下載