php接口限流实现方法

因为现在动不动就说高并发,说到高并发 就不得不提并发下限流、熔断、降级。
为什么要进行接口限流呢?
个人认为其实目的都是为了保证线上系统的稳定性,防止因为高频访问服务器而导致服务器宕机。

下面来简单实现一下接口限流的常用算法:

1.使用计数器进行限流

这应该是最简单也是最容易实现的,比如A接口1分钟内的访问次数不能超过100个。那么可以这么做:在一开始的时候,设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,就重置计数器。为了保证高并发下的原子性使用redis的incr实现计数器限流。

speedCounterApi.php文件

<?php
//计数器实现限流
class speedCounterApi
{
    //要访问的接口
    public function getApi()
    {
        $res = (new speedCounterApi())->SpeedCounter();
        $data = [
            'msg' => '获取成功',
            'code' => 200
        ];
        if (!$res) {
            $data['msg'] = '请稍后重试';
            $data['code'] = 400;
        }
        return $data;
    }
    /**
     * 1.redis 计数器实现方式
     * 限制1分钟内最大只能请求10次
     */
    public function SpeedCounter()
    {
        $redis = new \Redis();
        $redis->connect('10.225.137.105', 6379);
        $limitTime = 60;
        // 最大请求数量
        $maxCount = 10;
        $redisKey = 'api';
        // incr命令
        // 为键 key 储存的数字值加上一。
        // 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
        $count = $redis->incr($redisKey);
        print_r($count);
        echo PHP_EOL;
        //设置计算器的时间
        if ($count == 1) {
            //设置 Key -/s的过期时间
            $redis->expire($redisKey, $limitTime);
        }

        //1分钟内超过最大请求数
        if ($count > $maxCount) {
            return false;
        }
        return true;
    }
}
    $res = (new speedCounterApi())->getApi();
    print_r($res);
    echo PHP_EOL;

然后来执行一下查看效果

在这里插入图片描述
手动执行php文件 前面10次都可以访问后面10次我们主动拦截请求 粗略的看是实行了限流的方法。这方法存在的问题就是最后1秒内涌入所有请求,然后计数器过期重置后第一秒内又涌入大量请求 这样服务器还是可能会被高频访问搞挂。为了解决这种方法又出现了滑动窗口算法

2.滑动窗口算法

网上偷得图
百度拿的图,滑动窗口个人认为其实就是多存了时间,每次请求进来后时间范围之外的数据将被动态删除。主要使用redis的zset结构来实现

slideTimeWindow.php文件

<?php

//2.滑动窗口实现
class SlideTimeWindowApi
{
    //要访问的接口
    public function getApi()
    {
        $res = (new SlideTimeWindowApi())->SlideTimeWindow();
        $data = [
            'msg' => '获取成功',
            'code' => 200
        ];
        if (!$res) {
            $data['msg'] = '请稍后重试';
            $data['code'] = 400;
        }
        return $data;
    }


    /**
     * 2.redis 滑动窗口实现方式
     * 限制1分钟内最大只能请求10次
     * 使用redis事务保证redis原子性
     */
    public function SlideTimeWindow()
    {
        $redis = new \Redis();
        $redis->connect('10.225.137.105', 6379);
        $limitTime = 60;
        // 最大请求数量
        $maxCount = 10;
        $redisKey = 'slide_api';
        $nowTime = time();
        //使用管道提升性能
        $pipe = $redis->multi();
        //value 和 score 都使用时间戳,因为相同的元素会覆盖
        $sss=$pipe->zadd($redisKey, $nowTime, $nowTime);
        //移除时间窗口之前的行为记录,剩下的都是时间窗口内的
        $pipe->zremrangebyscore($redisKey, 0, $nowTime - $limitTime);
        //获取窗口内的行为数量
        $pipe->zcard($redisKey);
        //多加一秒过期时间
        $pipe->expire($redisKey, 60 + 1);
        //执行
        $replies = $pipe->exec();
        var_dump($replies[2]);
        //判断在有限时间窗口内的数量是否超过限制 true:false
        return $replies[2] <= $maxCount;
    }
}
$res = (new SlideTimeWindowApi())->getApi();
print_r($res);
echo PHP_EOL;

主要就是根据时间判断总体思路和计数器差不多
然后来执行一下查看效果

在这里插入图片描述
可以看到10次之后就拦截了请求。

3.漏斗算法

顾名思义漏斗,其实就是处理的速度是一定的。主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏斗算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。但是多余的请求将会被直接丢弃。
我更觉得是将流量控制在自己可以承受范围内,自己控制流量的速度。
处理请求的worker以固定的速度从桶中取出请求进行处理。
如果桶满了,直接返回请求频率超限的错误码或者页面
限流 体现在worker从桶中取请求的速度上
在这里插入图片描述
流量最均衡的限流实现方式。nginx的limit模块就是使用了漏斗算法

leackBucketApi.php文件

<?php

/**
 * 3.漏斗算法实现方式
 */
class leackBucketApi
{
    private $_water;    //漏斗的当前水量(也就是请求数)
    private $_burst=2;    //漏斗总量(超过将直接舍弃)
    private $_rate = 1; //漏斗出水速率(限流速度)
    private $_lastTime; //记录每次请求的时间(因为需要记录每次请求之间的出水量也就是请求数)
    private $_redis;    //redis对象

    public function __construct()
    {
        $this->_redis = new \Redis();
        $this->_redis->connect('10.225.137.105', 6379);
        $this->_lastTime = time(); //需要记录每次访问的时间
    }

    //要访问的接口
    public function getApi()
    {
        $res = (new leackBucketApi())->leackBucket();
        $data = [
            'msg' => '获取成功',
            'code' => 200
        ];
        if (!$res) {
            $data['msg'] = '请稍后重试';
            $data['code'] = 400;
        }
        return $data;
    }

    /**
     *
     */
    public function leackBucket()
    {
        $now = time();
        $redisKey = 'leackBucket_api';

        if (!empty($time = $this->_redis->get($redisKey))) {
            $this->_lastTime = $time; //获取上一次访问时间
        }
        if (!empty($water = $this->_redis->get('water'))) {
            $this->_water = $water;//获取当前剩余量也就是请求数
        }
        //计算出水量
        //因为rate是固定的,所以可以认为“时间间隔 * rate”即为漏出的水量
        $s=$now - $this->_lastTime; //当前时间减去上次访问时间,得到时间间隔
        $outCount=$s * $this->_rate; //漏出的水量(请求数)
        //执行漏水,计算剩余水量,也就是当前请求
        $this->_water=($this->_water - $outCount);
        if($this->_water<0){
            $this->_water=0; //重置为0
        }
        var_dump($this->_water.'----请求数');
        //请求数超出了突发请求限制
        if($this->_water > $this->_burst){
            echo "超出桶限制".PHP_EOL;
            return  false;
        }
        $this->_lastTime = $now; // 更新时间
        $water++;//更新待请求数
        $this->_redis->set($redisKey,$now);
        //记录请求数
        $this->_redis->set('water',$water);
        return true;
    }
}

$demo=new leackBucketApi();

//模拟请求
while (true){
    $time=mt_rand(600000,3600000);
    echo "'执行请求结果:".PHP_EOL;
    var_dump($demo->getApi());
    var_dump('随机时间:'.$time);
    usleep($time); //随机时间
}





然后来执行一下查看效果
在这里插入图片描述
不足之处在于:
面对突发流量时会有大量请求失败,但是我们可以使用预先准备好的资源返回。

4.令牌桶算法

在这里插入图片描述
令牌桶算法在漏桶算法流量处理均衡的基础上,允许一定程度的突发流量。
适合电商抢购或者微博出现热点事件这种场景。
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。
假设限制r/s,表示每秒会有r个令牌放入桶中,或者说每过1/r秒桶中增加一个令牌 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么在缓冲区等待)

对于令牌桶中令牌的产生一般有两种做法:
1、开启一个定时任务,由定时任务持续生成令牌。这样的问题在于会极大的消耗系统资源,如,某接口需要分别对每个用户做访问频率限制,假设系统中 存在6W用户,则至多需要开启6W个定时任务来维持每个桶中的令牌数,这样的开销是巨大的。
2、在每次获取令牌之前计算,其实现思路为,根据时间来计算访问前后该段时间内可以生成多少令牌,将生成的令牌加入令牌桶中并 更新数据。这样一来,只需要在获取令牌时计算一次即可。

令牌桶的主要限流体现在放入令牌的速度。
tokenBucketApi.php文件

<?php

/**
 * 4.令牌桶算法实现
 */

class tokenBucketApi
{

    private $_lastTime;     //记录每次请求的时间
    private $_burst = 2;    // 桶的容量
    private $_rate = 1;     // 令牌放入的速度
    private $_tokens;       // 当前令牌的数量
    private $_redis;        //redis对象

    public function __construct()
    {
        $this->_redis = new \Redis();
        $this->_redis->connect('10.225.137.105', 6379);
        $this->_lastTime = time(); //需要记录每次访问的时间
    }

    //要访问的接口
    public function getApi()
    {
        $res = (new tokenBucketApi())->tokenBucket();
        $data = [
            'msg' => '获取成功',
            'code' => 200
        ];
        if (!$res) {
            $data['msg'] = '请稍后重试';
            $data['code'] = 400;
        }
        return $data;
    }

    public function tokenBucket()
    {
        $now = time();
        $redisKey = 'tokenBucket_api';
        if (!empty($time = $this->_redis->get($redisKey))) {
            $this->_lastTime = $time; //获取上一次访问时间
        }
        if (!empty($tokens = $this->_redis->get('tokens'))) {
            $this->_tokens = $tokens;//获取当前令牌的数量
        }

        //计算生成的令牌量
        //因为rate是固定的,所以可以认为“时间间隔 * rate”即为生成的令牌量
        $s = $now - $this->_lastTime; //当前时间减去上次访问时间,得到时间间隔
        //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
        $addTokens = $s * $this->_rate;
        //当前令牌数= 之前的桶内令牌数量+放入的令牌数量(不能超过桶的总量)
        $this->_tokens = min($this->_burst, $this->_tokens + $addTokens);
        echo "令牌量:".$this->_tokens;
        //桶里面还有令牌,请求正常处理
        $this->_lastTime = $now;
        $this->_redis->set($redisKey, $now);
        if ($this->_tokens < 1) {
            // 若不到1个令牌,则拒绝
            echo "拿不到令牌".PHP_EOL;
            return false;
        }
        // 还有令牌,领取令牌
        $this->_tokens -= 1;
        //记录请求数
        $this->_redis->set('tokens', $this->_tokens);
        return true;
    }
}

$demo = new tokenBucketApi();
var_dump($demo->getApi());
//模拟请求
//while (true) {
//   $time = mt_rand(600000, 3600000);
//  echo "'执行请求结果:" . PHP_EOL;
// var_dump($demo->getApi());
//var_dump('随机时间:' . $time);
//usleep($time); //随机时间
}

然后手动执行多次
在这里插入图片描述
在这里插入图片描述
可以看到当慢慢执行的话 一直可以获取到令牌 因为令牌桶里一直有令牌,当请求过快将会丢弃(当然最好是返回我们预定好的资源)。过段时间令牌桶里又有了令牌又可以获取到令牌请求api。

放一张他们的对比图。
在这里插入图片描述

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值