一、什么是接口限流
那么什么是限流呢?顾名思义,限流就是限制流量,包括并发的流量和一定时间内的总流量,就像你宽带包了1个G的流量,用完了就没了,所以控制你的使用频率和单次使用的总消耗。

通过限流,我们可以很好地控制系统的qps,从而达到保护系统或者接口服务器稳定的目的。

二、常用的接口限流算法
1、计数器法
2、漏桶算法
3、令牌桶算法

今天主要使用令牌桶算法进行接口限流(基于laravel框架)

首先,我们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的(可用token数为0),token以一个固定的速率r往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话,请求无法通过。

Resilience4j 自定义接口限流_接口限流


令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.

代码实现:
1、通过定时任务,匀速的向令牌桶中添加令牌

<?php

namespace App\Console\Commands;

use App\Http\Controllers\Api\V1\TokenBucketController;
use Illuminate\Console\Command;


class CrontabAddTokens extends Command
{
    /**
     * 定时往令牌桶里添加令牌
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'CrontabTokens:add';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '定时自动增加令牌数';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {

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

        // 最大令牌数
        $max = 100;

        // 每次时间间隔加入的令牌数
        $token_num = 10;

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

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

        // 创建TokenBucketController对象
        $tokenBucket = new TokenBucketController($queue, $max);

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

    }


}

2、创建一个TokenBucketController对象,用户添加和消费令牌以及一些初始化设置

<?php
namespace App\Http\Controllers\Api\V1;


use Illuminate\Support\Facades\Redis;


class TokenBucketController extends BaseController
{

    private $_queue;  // 令牌桶
    private $_max;    // 最大令牌数

    /**
     * 初始化
     * @param Array $config redis连接设定
     */
    public function __construct($queue, $max = 10){

        $this->_queue = $queue;
        $this->_max = $max;
    }

    /**
     * 加入令牌
     * @param  Int $num 加入的令牌数量
     * @return Int 加入的数量
     */
    public function add($num=0)
    {

        // 当前剩余令牌数
        $curnum = intval(Redis::Llen($this->_queue));


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

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

        // 加入令牌
        if($num>0){
            $token = array_fill(0, $num, 1);
            Redis::lPush($this->_queue, ...$token);
            return $num;
        }

        return 0;

    }

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

    /**
     * 重设令牌桶,填满令牌
     */
    public function reset(){
        Redis::del($this->_queue);
        $this->add($this->_max);
    }

}

3、模拟用户请求消费令牌

<?php
namespace App\Http\Controllers\Api\V1;


use App\Component\ApiReturn;
use App\Component\Logic\GoodsLogic;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Response;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;


class TokenConsumeController extends BaseController
{

    public function consume()
    {
		$mycontainer = 'mycontainer';
        //获取当前令牌桶状态,是否获取到令牌
        $status = $this->getMycontainerStatus($mycontainer );
        if($status){
            //取到令牌,则查询数据
            $query = (new GoodsLogic())->getInfo(['id' => 1]);
            return ApiReturn::success($query);
        }else{
            //未取到令牌,则返回服务繁忙
            return Response::json([
                'msg' => '服务器繁忙,请稍后再试'
            ], 500); // Status code here
        }


    }
    
    /**
     * 获取令牌桶令牌状态
     * @param string $queue
     * @return bool
     */
    public function getMycontainerStatus($queue = 'mycontainer')
    {
        // 令牌桶容器
//        $queue = 'mycontainer';
        // 创建TokenBucketController对象
        $tokenBucket = new TokenBucketController($queue);

        $status = $tokenBucket->get();
        return $status;
    }


}