PHPIN.NET

 找回密码
 立即注册
查看: 5885|回复: 0

[PHP类\函数] php 基于redis使用令牌桶算法实现流量控制

[复制链接]

469

主题

31

回帖

5569

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
5569
发表于 2018-5-21 12:34:06 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x

本文介绍php基于redis,使用令牌桶算法,实现访问流量的控制,提供完整算法说明及演示实例,方便大家学习使用。


每当国内长假期或重要节日时,国内的景区或地铁都会人山人海,导致负载过大,部分则会采用限流措施,限制进入的人数,当区内人数降低到一定值,再允许进入。

例如:
区内最大允许人数为 M
区内当前人数为 N
每进入一个人,N+1,当N = M时,则不允许进入
每离开一个人,N-1,当N < M时,可允许进入


系统在运行过程中,如遇上某些活动,访问的人数会在一瞬间内爆增,导致服务器瞬间压力飙升,使系统超负荷工作。

当然我们可以增加服务器去分担压力,首先增加服务器也需要一定的时间去配置,而且因为某一个活动而增加服务器,活动结束后这些服务器资源就浪费了。

因此我们可以根据业务类型,先使用限流的方式去减轻服务器压力。

与景区限流不同,系统的访问到结束的时间非常短,因此我们只需要知道每个访问持续的平均时间,设定最多同时访问的人数即可。

令牌桶算法

1.首先设有一个令牌桶,桶内存放令牌,一开始令牌桶内的令牌是满的(桶内令牌的数量可根据服务器情况设定)。

2.每次访问从桶内取走一个令牌,当桶内令牌为0,则不允许再访问。

3.每隔一段时间,再放入令牌,最多使桶内令牌满额。(可以根据实际情况,每隔一段时间放入若干个令牌,或直接补满令牌桶)

我们可以使用redis的队列作为令牌桶容器使用,使用lPush(入队),rPop(出队),实现令牌加入与消耗的操作。
  
TrafficShaper.class.php

  1. <?php
  2. /**
  3. * PHP基于Redis使用令牌桶算法实现流量控制
  4. * Date:    2018-02-23
  5. * Author:  fdipzone
  6. * Version: 1.0
  7. *
  8. * Descripton:
  9. * php基于Redis使用令牌桶算法实现流量控制,使用redis的队列作为令牌桶容器,入队(lPush)出队(rPop)作为令牌的加入与消耗操作。
  10. *
  11. * Func:
  12. * public  add     加入令牌
  13. * public  get     获取令牌
  14. * public  reset   重设令牌桶
  15. * private connect 创建redis连接
  16. */
  17. class TrafficShaper{ // class start

  18.     private $_config; // redis设定
  19.     private $_redis;  // redis对象
  20.     private $_queue;  // 令牌桶
  21.     private $_max;    // 最大令牌数

  22.     /**
  23.      * 初始化
  24.      * @param Array $config redis连接设定
  25.      */
  26.     public function __construct($config, $queue, $max){
  27.         $this->_config = $config;
  28.         $this->_queue = $queue;
  29.         $this->_max = $max;
  30.         $this->_redis = $this->connect();
  31.     }

  32.     /**
  33.      * 加入令牌
  34.      * @param  Int $num 加入的令牌数量
  35.      * @return Int 加入的数量
  36.      */
  37.     public function add($num=0){

  38.         // 当前剩余令牌数
  39.         $curnum = intval($this->_redis->lSize($this->_queue));

  40.         // 最大令牌数
  41.         $maxnum = intval($this->_max);

  42.         // 计算最大可加入的令牌数量,不能超过最大令牌数
  43.         $num = $maxnum>=$curnum+$num? $num : $maxnum-$curnum;

  44.         // 加入令牌
  45.         if($num>0){
  46.             $token = array_fill(0, $num, 1);
  47.             $this->_redis->lPush($this->_queue, ...$token);
  48.             return $num;
  49.         }

  50.         return 0;

  51.     }

  52.     /**
  53.      * 获取令牌
  54.      * @return Boolean
  55.      */
  56.     public function get(){
  57.         return $this->_redis->rPop($this->_queue)? true : false;
  58.     }

  59.     /**
  60.      * 重设令牌桶,填满令牌
  61.      */
  62.     public function reset(){
  63.         $this->_redis->delete($this->_queue);
  64.         $this->add($this->_max);
  65.     }

  66.     /**
  67.      * 创建redis连接
  68.      * @return Link
  69.      */
  70.     private function connect(){
  71.         try{
  72.             $redis = new Redis();
  73.             $redis->connect($this->_config['host'],$this->_config['port'],$this->_config['timeout'],$this->_config['reserved'],$this->_config['retry_interval']);
  74.             if(empty($this->_config['auth'])){
  75.                 $redis->auth($this->_config['auth']);
  76.             }
  77.             $redis->select($this->_config['index']);
  78.         }catch(RedisException $e){
  79.             throw new Exception($e->getMessage());
  80.             return false;
  81.         }
  82.         return $redis;
  83.     }


  84. } // class end
  85. ?>
复制代码

demo:
  1. <?php
  2. /**
  3. * 演示令牌加入与消耗
  4. */
  5. require 'TrafficShaper.class.php';

  6. // redis连接设定
  7. $config = array(
  8.     'host' => 'localhost',
  9.     'port' => 6379,
  10.     'index' => 0,
  11.     'auth' => '',
  12.     'timeout' => 1,
  13.     'reserved' => NULL,
  14.     'retry_interval' => 100,
  15. );

  16. // 令牌桶容器
  17. $queue = 'mycontainer';

  18. // 最大令牌数
  19. $max = 5;

  20. // 创建TrafficShaper对象
  21. $oTrafficShaper = new TrafficShaper($config, $queue, $max);

  22. // 重设令牌桶,填满令牌
  23. $oTrafficShaper->reset();

  24. // 循环获取令牌,令牌桶内只有5个令牌,因此最后3次获取失败
  25. for($i=0; $i<8; $i++){
  26.     var_dump($oTrafficShaper->get());
  27. }

  28. // 加入10个令牌,最大令牌为5,因此只能加入5个
  29. $add_num = $oTrafficShaper->add(10);

  30. var_dump($add_num);

  31. // 循环获取令牌,令牌桶内只有5个令牌,因此最后1次获取失败
  32. for($i=0; $i<6; $i++){
  33.     var_dump($oTrafficShaper->get());
  34. }

  35. ?>
复制代码
输出:
  1. boolean true
  2. boolean true
  3. boolean true
  4. boolean true
  5. boolean true
  6. boolean false
  7. boolean false
  8. boolean false
  9. int 5
  10. boolean true
  11. boolean true
  12. boolean true
  13. boolean true
  14. boolean true
  15. boolean false
复制代码
定期加入令牌算法

定期加入令牌,我们可以使用crontab实现,每分钟调用add方法加入若干令牌。crontab的使用可以参考:《Linux crontab定时执行任务 命令格式与详细例子》

crontab最小的执行间隔为1分钟,如果令牌桶内的令牌在前几秒就已经被消耗完,那么剩下的几十秒时间内,都获取不到令牌,导致用户等待时间较长。

我们可以优化加入令牌的算法,改为一分钟内每若干秒加入若干令牌,这样可以保证一分钟内每段时间都有机会能获取到令牌。
  
crontab调用的加入令牌程序如下,每秒自动加入3个令牌。

  1. <?php
  2. /**
  3. * 定时任务加入令牌
  4. */
  5. require 'TrafficShaper.class.php';

  6. // redis连接设定
  7. $config = array(
  8.     'host' => 'localhost',
  9.     'port' => 6379,
  10.     'index' => 0,
  11.     'auth' => '',
  12.     'timeout' => 1,
  13.     'reserved' => NULL,
  14.     'retry_interval' => 100,
  15. );

  16. // 令牌桶容器
  17. $queue = 'mycontainer';

  18. // 最大令牌数
  19. $max = 10;

  20. // 每次时间间隔加入的令牌数
  21. $token_num = 3;

  22. // 时间间隔,最好是能被60整除的数,保证覆盖每一分钟内所有的时间
  23. $time_step = 1;

  24. // 执行次数
  25. $exec_num = (int)(60/$time_step);

  26. // 创建TrafficShaper对象
  27. $oTrafficShaper = new TrafficShaper($config, $queue, $max);

  28. for($i=0; $i<$exec_num; $i++){
  29.     $add_num = $oTrafficShaper->add($token_num);
  30.     echo '['.date('Y-m-d H:i:s').'] add token num:'.$add_num.PHP_EOL;
  31.     sleep($time_step);
  32. }

  33. ?>
复制代码
模拟消耗程序如下,每秒消耗2-8个令牌。
  1. <?php
  2. /**
  3. * 模拟用户访问消耗令牌,每段时间间隔消耗若干令牌
  4. */
  5. require 'TrafficShaper.class.php';

  6. // redis连接设定
  7. $config = array(
  8.     'host' => 'localhost',
  9.     'port' => 6379,
  10.     'index' => 0,
  11.     'auth' => '',
  12.     'timeout' => 1,
  13.     'reserved' => NULL,
  14.     'retry_interval' => 100,
  15. );

  16. // 令牌桶容器
  17. $queue = 'mycontainer';

  18. // 最大令牌数
  19. $max = 10;

  20. // 每次时间间隔随机消耗的令牌数量范围
  21. $consume_token_range = array(2, 8);

  22. // 时间间隔
  23. $time_step = 1;

  24. // 创建TrafficShaper对象
  25. $oTrafficShaper = new TrafficShaper($config, $queue, $max);

  26. // 重设令牌桶,填满令牌
  27. $oTrafficShaper->reset();

  28. // 执行令牌消耗
  29. while(true){
  30.     $consume_num = mt_rand($consume_token_range[0], $consume_token_range[1]);
  31.     for($i=0; $i<$consume_num; $i++){
  32.         $status = $oTrafficShaper->get();
  33.         echo '['.date('Y-m-d H:i:s').'] consume token:'.($status? 'true' : 'false').PHP_EOL;
  34.     }
  35.     sleep($time_step);
  36. }

  37. ?>
复制代码

演示

设置定时任务,每分钟执行一次

  1. //设置定时任务,每分钟执行一次
  2. * * * * * php /程序的路径/cron_add.php >> /tmp/cron_add.log
  3. //执行模拟消耗
  4. php consume_demo.php
  5. //执行结果:
  6. [2018-02-23 11:42:57] consume token:true
  7. [2018-02-23 11:42:57] consume token:true
  8. [2018-02-23 11:42:57] consume token:true
  9. [2018-02-23 11:42:57] consume token:true
  10. [2018-02-23 11:42:57] consume token:true
  11. [2018-02-23 11:42:57] consume token:true
  12. [2018-02-23 11:42:57] consume token:true
  13. [2018-02-23 11:42:58] consume token:true
  14. [2018-02-23 11:42:58] consume token:true
  15. [2018-02-23 11:42:58] consume token:true
  16. [2018-02-23 11:42:58] consume token:true
  17. [2018-02-23 11:42:58] consume token:true
  18. [2018-02-23 11:42:58] consume token:true
  19. [2018-02-23 11:42:58] consume token:false
  20. [2018-02-23 11:42:59] consume token:true
  21. [2018-02-23 11:42:59] consume token:true
  22. [2018-02-23 11:42:59] consume token:true
  23. [2018-02-23 11:42:59] consume token:false
  24. [2018-02-23 11:42:59] consume token:false
  25. [2018-02-23 11:42:59] consume token:false
  26. [2018-02-23 11:42:59] consume token:false
  27. [2018-02-23 11:43:00] consume token:true
  28. [2018-02-23 11:43:00] consume token:true
  29. [2018-02-23 11:43:00] consume token:true
  30. [2018-02-23 11:43:00] consume token:false
  31. [2018-02-23 11:43:00] consume token:false
复制代码

因令牌桶一开始是满的(最大令牌数10),所以之前的10次都能获取到令牌,10次之后则会根据消耗的令牌大于加入令牌数时,限制访问。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|PHPIN.NET ( 冀ICP备12000898号-14 )|网站地图

GMT+8, 2024-10-31 06:36

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表