晓夏

北漂的女孩

Good Luck To You!

流量并发控制常用的方法

浏览量:311

在最近几天的面试中,老被面试问到,对于大流量高并发的情况下,怎么做到并发流量控制

1.计数器 

<?php
/**
 * 计数器来限流
 * @param $uid  用户的uid
 * @param $actionKey  任务名的key
 * @param int $limitTime 多长时间
 * @param int $maxCount 访问的最大次数
 */
function countingFlow($uid,$actionKey,$limitTime=60,$maxCount=100){
    if(empty($uid) || empty($actionKey)){
        return false;
    }
    $redis = new Redis();
    $redis->connect("127.0.0.1",6379);
    $redis->auth("foobared");
    $redisKey = sprintf("hist:%s:%s",$uid,$actionKey);
    $count = $redis->incr($redisKey);
    if($count == 1){
        $redis->expire($redisKey,$limitTime);
    }
    if($count > $maxCount){
        return false;
    }
    return true;
}

注意:

临界问题:
在第一次0-58秒之内有1个请求,在59秒内突然来了15个请求,这个时候已经到了1分钟计数器会重置。
第二次的1秒内(1分0秒)又有了15个请求,这个时候是不是就在2秒内有30个请求被放行了呢?(59秒,1分0秒),如果某个服务器的访问只能是20次请求,那么这种限流方式已经导致服务器挂掉了。

2.滑动窗口计数器

<?php
/**
 * 滑动窗口计数器来限流
 * @param $uid  用户的uid
 * @param $actionKey  任务名的key
 * @param int $period 时间 毫秒
 * @param int $maxCount 最大次数
 */
function isActionAllowed($uid,$actionKey,$period,$maxCount=100){
    if(empty($uid) || empty($actionKey)){
        return false;
    }
    $redis = new Redis();
    $redis->connect("127.0.0.1",6379);
    $redis->auth('foobared'); //密码验证

    $now = msectime();//毫秒时间戳
    $maxCount = 1;
    //拼接key
    $redisKey = sprintf('miaosha:%s:%s',$uid,$actionKey);
    //事务开始客户端请求 客户端缓存
    $pipe = $redis->multi(Redis::PIPELINE);
    //value 和 score 都用毫秒时间戳
    $pipe->zAdd($redisKey,$now,$now);
    //移除时间窗口之前的行为记录,剩下的都是时间窗口
    $pipe->zRemRangeByScore($redisKey,0,$now - $period);
    $pipe->zCard($redisKey);
    //获取窗口内的行为数量
    //多加1秒的过期时间
    $pipe->expire($redisKey,$period+1);
    $res = $pipe->exec();

    return $res[2] < $maxCount;

}

3.令牌桶限流

  •  所有的请求之前都需要那个令牌

  •  根据限流的大小按照一定的速率往桶中放令牌

  •  桶需要限制一个最大的令牌数,当桶满的的时候,新增的令牌就得被丢弃或者拒绝

  • 请求先拿着令牌才能进行自己的业务逻辑然后处理完删除

  • 令牌有最低限额,当桶中的限额达到最低令牌时,请求处理完也不会删除令牌,以达到最终限流

<?php
<?php

class Token
{
    private $_max;//最大的令牌
    private $_queue;
    private $_redis;

    public function __construct()
    {
        try {
            $this->_redis = new Redis();
            $this->_redis->connect("127.0.0.1", 6379);
            $this->_redis->auth("foobared");

            $this->_queue = "token";
            $this->_max = 10;

        } catch (RedisException $exception) {
            throw  new Exception($exception->__toString());
            return false;
        }
    }

    /**
     * 令牌初始化
     */
    public function resertKey()
    {
        $this->_redis->del($this->_queue);
        $this->_add($this->_max);
    }

    /**
     * 添加令牌
     * @param int $number
     */
    public function add($number = 1)
    {
        $maxNum = $this->_max;
        $currentNumber = $this->_redis->lLen($this->_queue);
        if ($maxNum > $currentNumber + $number) {
            $number = $number;
        } else {
            $number = $maxNum - $currentNumber;
        }
        if ($number > 0) {
            $tokens = array_fill(0, $number, 1);

            foreach ($tokens as $token) {
                $this->_redis->lPush($this->_queue, $token);
            }
        }
    }

    /**
     * 获取令牌
     * @return bool
     */
    public function get()
    {
        return $this->_redis->rPop($this->_queue) ? true : false;
    }
}

消费类:模拟用户请求

<?php
require 'token.php';
$token = new Token();
var_dump($token->get());

投递类:后端向令牌桶投递

<?php
require 'token.php';
$token = new Token();
$token->add(1);




神回复

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。