Laravel API throttle 原理分析

Epona發表於2019-04-18

Laravel 自從5.2版本起就加入了throttle中介軟體來進行限流。下面我們看一下具體的原理是怎樣實現的。

ThrottleRequests

throttle 中介軟體的class為Illuminate\Routing\Middleware\ThrottleRequests。程式碼如下:

class ThrottleRequests 
{

     /**
     * The rate limiter instance.
     *
     * @var \Illuminate\Cache\RateLimiter
     */
    protected $limiter;

    /**
     * Create a new request throttler.
     *
     * @param  \Illuminate\Cache\RateLimiter  $limiter
     * @return void
     */
    public function __construct(RateLimiter $limiter)
    {
        $this->limiter = $limiter;
    }

     ...
}

在建構函式中初始化了一個 RateLimiter 類,程式碼如下:

class RateLimiter
{
    protected $cache;

    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }
}

這表示處理限流的相關資訊儲存位置與我們在專案中配置的 cache driver 一致。接著回到ThrottleRequests中。

// ThrottleRequests
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
    $key = $this->resolveRequestSignature($request);

    $maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);

    if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
        throw $this->buildException($key, $maxAttempts);
    }

    $this->limiter->hit($key, $decayMinutes);

    $response = $next($request);

    return $this->addHeaders(
        $response, $maxAttempts,
        $this->calculateRemainingAttempts($key, $maxAttempts)
    );
}

protected function resolveRequestSignature($request)
{
    if ($user = $request->user()) {
        return sha1($user->getAuthIdentifier());
    }

    if ($route = $request->route()) {
        return sha1($route->getDomain().'|'.$request->ip());
    }

    throw new RuntimeException(
        'Unable to generate the request signature. Route unavailable.'
    );
}

// RateLimiter
public function tooManyAttempts($key, $maxAttempts, $decayMinutes = 1)
{
    if ($this->attempts($key) >= $maxAttempts) {
        if ($this->cache->has($key.':timer')) {
            return true;
        }

        $this->resetAttempts($key);
    }

    return false;
}

public function hit($key, $decayMinutes = 1)
{
    $this->cache->add(
        $key.':timer', $this->availableAt($decayMinutes * 60), $decayMinutes
    );

    $added = $this->cache->add($key, 0, $decayMinutes);

    $hits = (int) $this->cache->increment($key);

    if (! $added && $hits == 1) {
        $this->cache->put($key, 1, $decayMinutes);
    }

    return $hits;
}

和我們熟知的中介軟體一樣,throttle 也透過 handle 方法來進行處理。其中 key 返回的是一個使用者標記或者ip標記。即如果使用者已經登入,那麼根據使用者來判斷限流,否則根據IP進行限流。接著判斷是否達到了限流的上限,如果達到上限丟擲異常。否則針對指定的 key 將訪問次數加1. 然後在響應中加入指定的 HEADER 後返回。至此,中介軟體流程基本結束。

HEADERS

在加入限流中介軟體之後,會在API中加入特定的HEADER,例如我們設定某個路由的中介軟體如下:

$api->get('demo', 'xxxxController@xx')->middleware('throttle:5,1');

這表示 demo 這個 API 每個使用者每1分鐘只能訪問5次。那麼在我們訪問的時候會看到在響應中加入了2個新的HEADER。

X-RateLimit-Limit: 5
X-RateLimit-Remaining: 4

表示路由的API限制次數為5次,還可以訪問4次。當我們達到訪問上限之後再次訪問API,就會得到如下的HEADER:

X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
Retry-After: 43
X-RateLimit-Reset: 1555506730

新增加的兩個HEADER中,Retry-After 表示我們可以在43秒後重新訪問,限流的限制將在 X-RateLimit-Reset 返回的時間戳進行重置。

EXTRA

其實 Laravel 還為我們提供了 ThrottleRequestsWithRedis 類,所實現的功能與前面一致,只不過使用了 Redis 來進行資料儲存。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
There's nothing wrong with having a little fun.

相關文章