代码之家  ›  专栏  ›  技术社区  ›  Salman Arshad

将功能限制为每分钟最多执行100次

  •  1
  • Salman Arshad  · 技术社区  · 7 年前

    我有一个脚本,可以向API发出多个POST请求。剧本大致概述如下:

    define("MAX_REQUESTS_PER_MINUTE", 100);
    
    function apirequest ($data) {
        // post data using cURL
    }
    
    while ($data = getdata ()) {
        apirequest($data);
    }
    

    API受到限制,允许用户每分钟最多发布100个请求。其他请求返回HTTP错误并在响应后重试,直到 窗口重置 . 请注意,服务器处理请求可能需要100毫秒到100秒之间的任何时间。

    我需要确保我的函数每分钟执行的次数不超过100次。我试过了 usleep 函数引入0.66秒的恒定延迟,但这只是每分钟增加一分钟。任意值(如0.1秒)会导致一次或另一次错误。我将所有请求随时间记录在一个数据库表中,我使用的另一个解决方案是探测该表并统计过去60秒内发出的请求数。

    我需要一个尽可能少浪费时间的解决方案。

    4 回复  |  直到 6 年前
        1
  •  2
  •   ob-ivan    7 年前

    我已经把德里克的建议写成代码了。

    class Throttler {
        private $maxRequestsPerMinute;
        private $getdata;
        private $apirequest;
    
        private $firstRequestTime = null;
        private $requestCount = 0;
    
        public function __construct(
            int $maxRequestsPerMinute,
            $getdata,
            $apirequest
        ) {
            $this->maxRequestsPerMinute = $maxRequestsPerMinute;
            $this->getdata = $getdata;
            $this->apirequest = $apirequest;
        }
    
        public function run() {
            while ($data = call_user_func($this->getdata)) {
                if ($this->requestCount >= $this->maxRequestsPerMinute) {
                    sleep(ceil($this->firstRequestTime + 60 - microtime(true)));
                    $this->firstRequestTime = null;
                    $this->requestCount = 0;
                }
                if ($this->firstRequestTime === null) {
                    $this->firstRequestTime = microtime(true);
                }
                ++$this->requestCount;
                call_user_func($this->apirequest, $data);
            }
        }
    }
    
    $throttler = new Throttler(100, 'getdata', 'apirequest');
    $throttler->run();
    

    UPD。 我已将其更新版本放在Packagegist上,以便您可以将其与Composer一起使用: https://packagist.org/packages/ob-ivan/throttler

    要安装:

    composer require ob-ivan/throttler
    

    要使用:

    use Ob_Ivan\Throttler\JobInterface;
    use Ob_Ivan\Throttler\Throttler;
    
    class SalmanJob implements JobInterface {
        private $data;
        public function next(): bool {
            $this->data = getdata();
            return (bool)$this->data;
        }
        public function execute() {
            apirequest($this->data);
        }
    }
    
    $throttler = new Throttler(100, 60);
    $throttler->run(new SalmanJob());
    

    请注意,还有其他提供相同功能的软件包(我没有测试过任何一个):

        2
  •  2
  •   Derek Joseph Olson    7 年前

    我会先记录第一个请求发出的初始时间,然后计算发出的请求数。发出60个请求后,确保当前时间至少比初始时间晚1分钟。如果没有,请等待多长时间,直到到达分钟。达到分钟时,重置计数和初始时间值。

        3
  •  1
  •   Salman Arshad    7 年前

    我的做法是:

    define("MAX_REQUESTS_PER_MINUTE", 100);
    
    function apirequest() {
        static $startingTime;
        static $requestCount;
        if ($startingTime === null) {
            $startingTime = time();
        }
        if ($requestCount === null) {
            $requestCount = 0;
        }
    
        $consumedTime = time() - $startingTime;
    
        if ($consumedTime >= 60) {
            $startingTime = time();
            $requestCount = 0;
        } elseif ($requestCount === MAX_REQUESTS_PER_MINUTE) {
            sleep(60 - $consumedTime);
            $startingTime = time();
            $requestCount = 0;
        }
        $requestCount++;
    
        echo sprintf("Request %3d, Range [%d, %d)", $requestCount, $startingTime, $startingTime + 60) . PHP_EOL;
        file_get_contents("http://localhost/apirequest.php");
        // the above script sleeps for 200-400ms
    }
    
    for ($i = 0; $i < 1000; $i++) {
        apirequest();
    }
    
        4
  •  0
  •   Sammitch    7 年前

    我尝试过静态睡眠、计算请求数和做简单的数学运算等天真的解决方案,但它们往往非常不准确、不可靠,通常会引入更多的睡眠,而这在他们本可以工作的时候是必要的。你想要的是,只有当你接近利率上限时,才会出现相应的睡眠。

    将我的解决方案从 previous problem 对于那些甜蜜的互联网点:


    我用一些数学计算出一个函数,该函数将在给定的请求中休眠正确的时间总和,并允许我在接近结束时以指数方式递增。

    如果我们将睡眠表示为:

    y = e^( (x-A)/B )
    

    哪里 A B 是控制曲线形状的任意值,然后是所有休眠的总和, M 从…起 0 N 请求将是:

    M = 0∫N e^( (x-A)/B ) dx
    

    这相当于:

    M = B * e^(-A/B) * ( e^(N/B) - 1 )
    

    并可通过以下方式解决:

    A = B * ln( -1 * (B - B * e^(N/B)) / M )
    

    而求解B会更有用,因为 A. 让你定义一个图形急剧上升的点,这个点的解在数学上很复杂,我自己也找不到任何人 其他的 那可以。

    /**
     * @param int $period   M, window size in seconds
     * @param int $limit    N, number of requests permitted in the window
     * @param int $used x, current request number
     * @param int $bias B, "bias" value
     */
    protected static function ratelimit($period, $limit, $used, $bias=20) {
        $period = $period * pow(10,6);
        $sleep = pow(M_E, ($used - self::biasCoeff($period, $limit, $bias))/$bias);
        usleep($sleep);
    }
    
    protected static function biasCoeff($period, $limit, $bias) {
        $key = sprintf('%s-%s-%s', $period, $limit, $bias);
        if( ! key_exists($key, self::$_bcache) ) {
            self::$_bcache[$key] = $bias * log( -1 * ( ($bias - $bias * pow(M_E, $limit/$bias)) / $period ) );
        }
        return self::$_bcache[$key];
    }
    

    经过一点修补,我发现 B = 20 虽然我没有数学基础,但这似乎是一个不错的违约。 什么什么什么什么 斜坡 咕哝咕哝 指数型 bs bs .

    还有,如果有人想解这个方程 B 为了我 I've got a bounty up on math.stackexchange .


    虽然我相信我们的情况略有不同,因为我的API提供程序的响应都包括可用API调用的数量,以及窗口中剩余的数量。您可能需要额外的代码来跟踪这一点。