Laravel 佇列原始碼解析

Remember發表於2019-11-16

我總是控制不好自己的情緒。其實,情緒只是對自己無能的憤怒罷了。

Laravel 佇列原始碼解析

開篇

日常開發使用佇列的場景不少了吧,至於如何使用,我想文件已經寫的很清楚了,畢業一年多了,七月份換一家新公司的時候開始使用 Laravel,因為專案中場景經常使用到 Laravel 中的佇列,結合自己閱讀的一絲佇列的原始碼,寫了這篇文章。(公司一直用的5.5 所以文章的版本你懂的。)

也不知道從哪講起,那就從一個最基礎的例子開始吧。建立一個最簡單的任務類 SendMessage。繼承Illuminate\Contracts\Queue\ShouldQueue 介面。

簡單 demo 開始

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;

class SendMessage implements ShouldQueue
{

    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $message;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($message)
    {
        $this->message = $message;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Log::info($this->message);
    }
}

這裡直接分發一個任務類到佇列中,並沒有指定分發到哪個佇列,那麼會直接分發給預設的佇列。直接從這裡開始分析吧。

 public function test()
    {
        $msg = '吳親庫裡';
        SendMessage::dispatch($msg);
    }

首先 SendMessage 並沒有 dispatch 這個靜態方法, 但是它 use dispatchable 這樣的 Trait 類,我們可以點開 dispatchable 類檢視 dispatch 方法。


trait Dispatchable
{
    /**
     * Dispatch the job with the given arguments.
     *
     * @return \Illuminate\Foundation\Bus\PendingDispatch
     */
    public static function dispatch()
{
        return new PendingDispatch(new static(...func_get_args()));
    }

    /**
     * Set the jobs that should run if this job is successful.
     *
     * @param  array  $chain
     * @return \Illuminate\Foundation\Bus\PendingChain
     */
    public static function withChain($chain)
{
        return new PendingChain(get_called_class(), $chain);
    }
}

可以看到在 dispatch 方法中 例項化另一個 PendingDispatch 類,並且根據傳入的引數例項化任務 SendMessage 作為 PendingDispatch 類的引數。我們接著看,它是咋麼分派任務?外層控制器現在只呼叫了 dispatch,看看 PendingDispatch 類中有什麼


<?php

namespace Illuminate\Foundation\Bus;

use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Facades\Log;

class PendingDispatch
{
    /**
     * The job.
     *
     * @var mixed
     */
    protected $job;

    /**
     * Create a new pending job dispatch.
     *
     * @param  mixed  $job
     * @return void
     */
    public function __construct($job)
{
        $this->job = $job;
    }

    /**
     * Set the desired connection for the job.
     *
     * @param  string|null  $connection
     * @return $this
     */
    public function onConnection($connection)
{
        $this->job->onConnection($connection);

        return $this;
    }

    /**
     * Set the desired queue for the job.
     *
     * @param  string|null  $queue
     * @return $this
     */
    public function onQueue($queue)
{

        $this->job->onQueue($queue);

        return $this;
    }

    /**
     * Set the desired connection for the chain.
     *
     * @param  string|null  $connection
     * @return $this
     */
    public function allOnConnection($connection)
{
        $this->job->allOnConnection($connection);

        return $this;
    }

    /**
     * Set the desired queue for the chain.
     *
     * @param  string|null  $queue
     * @return $this
     */
    public function allOnQueue($queue)
{
        $this->job->allOnQueue($queue);

        return $this;
    }

    /**
     * Set the desired delay for the job.
     *
     * @param  \DateTime|int|null  $delay
     * @return $this
     */
    public function delay($delay)
{
        $this->job->delay($delay);

        return $this;
    }

    /**
     * Set the jobs that should run if this job is successful.
     *
     * @param  array  $chain
     * @return $this
     */
    public function chain($chain)
{
        $this->job->chain($chain);

        return $this;
    }

    /**
     * Handle the object's destruction.
     *
     * @return void
     */
    public function __destruct()
{
        app(Dispatcher::class)->dispatch($this->job);
    }
}

這裡檢視它的構造和解構函式。好吧從解構函式上已經能看出來,執行推送任務的在這裡,app(Dispatcher::class) 這又是什麼鬼?看來還得從執行機制開始看。Laravel 底層提供了一個強大的 IOC 容器,我們這裡通過輔助函式 app() 的形式訪問它,並通過傳遞引數解析出一個服務物件。這裡我們傳遞 Dispatcher::class 得到的是一個什麼服務?這個服務又是在哪裡被註冊進去的。讓我們把目光又轉移到根目錄下的 index.php 檔案。因為這篇文章不是說執行流程,所以一些流程會跳過。

註冊服務


/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

這個服務實際上執行了 handle 方法之後才有的(別問我為什麼,如果和我一樣笨的話,多打斷點?),這裡的 $kernel 實際上得到的是一個 

Illuminate\Foundation\Http\Kernel 類,讓我們進去看看這個類裡面的 handle 方法。

/**
     * Handle an incoming HTTP request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function handle($request)
  {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Exception $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        } catch (Throwable $e) {
            $this->reportException($e = new FatalThrowableError($e));

            $response = $this->renderException($request, $e);
        }

        $this->app['events']->dispatch(
            new Events\RequestHandled($request, $response)
        );

        return $response;
    }

這個方法主要是處理傳入的請求,追蹤一下 sendRequestThroughRouter 方法。

  /**
     * Send the given request through the middleware / router.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendRequestThroughRouter($request)
  {
        $this->app->instance('request', $request);

        Facade::clearResolvedInstance('request');

        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

其他的程式碼不是我們這篇文章討論範圍之內。主要追下 bootstarp()方法。方法名就很好理解了。


/**
     * Bootstrap the application for HTTP requests.
     *
     * @return void
     */
    public function bootstrap()
   {

        if (! $this->app->hasBeenBootstrapped()) {
            $this->app->bootstrapWith($this->bootstrappers());
        }

    }
    /**
     * Get the bootstrap classes for the application.
     *
     * @return array
     */
    protected function bootstrappers()
   {
        return $this->bootstrappers;
    }
 /**
     * The bootstrap classes for the application.
     *
     * @var array
     */
    protected $bootstrappers = [
        \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        \Illuminate\Foundation\Bootstrap\BootProviders::class,
    ];

應用程式初始化要引導的類,是一個陣列,傳入到已經存在的 Application 類中的 bootstrapWith 方法中,讓我們追蹤一下這個方法。

    /**
     * Run the given array of bootstrap classes.
     *
     * @param  array  $bootstrappers
     * @return void
     */
    public function bootstrapWith(array $bootstrappers)
   {
        $this->hasBeenBootstrapped = true;

        foreach ($bootstrappers as $bootstrapper) {
            $this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);

            $this->make($bootstrapper)->bootstrap($this);

            $this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);

        }
    }

遍歷傳入的陣列 $bootstrappers,繼續追蹤 $this->make 方法。

    /**
     * Resolve the given type from the container.
     *
     * (Overriding Container::make)
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     */
    public function make($abstract, array $parameters = [])
  {
        $abstract = $this->getAlias($abstract);

        if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
            $this->loadDeferredProvider($abstract);
        }

        return parent::make($abstract, $parameters);
    }

根據傳遞的引數,從容器中解析給定的型別獲取到例項物件。再回到上一步,呼叫每一個物件的 bootstrap 方法。我們主要看 RegisterProviders 中的 bootstrap 方法。


/**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
   {
        $app->registerConfiguredProviders();
    }

重新回到 ApplicationregisterConfiguredProviders 方法。

/**
     * Register all of the configured providers.
     *
     * @return void
     */
    public function registerConfiguredProviders()
 {
        $providers = Collection::make($this->config['app.providers'])
                        ->partition(function ($provider) {
                            return Str::startsWith($provider, 'Illuminate\\');
                        });

        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);

        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))
                    ->load($providers->collapse()->toArray());

    }

註冊所有配置提供的服務。因為這一塊程式碼過多,不是本章討論的範圍(其實是我這一塊有些地方還沒看懂?),所以主要看 $this->config['app.providers'],原來是要載入 app.phpproviders 裡面的陣列配置。

'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

    ],

讓我們點開  BusServiceProvider ,終於找到一開始想要的東西了,原來註冊的就是這個服務啊。

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
   {
        $this->app->singleton(Dispatcher::class, function ($app) {
            return new Dispatcher($app, function ($connection = null) use ($app) {
                return $app[QueueFactoryContract::class]->connection($connection);
            });
        });

        $this->app->alias(
            Dispatcher::class, DispatcherContract::class
        );

        $this->app->alias(
            Dispatcher::class, QueueingDispatcherContract::class
        );
    }

解析服務

所以之前的 app(Dispatcher::class) 解析的實際上是 BusServiceProvider 服務。

所以上面的解構函式實際上呼叫的是  Dispatcher 類中的 dispatch 方法。

    /**
     * Dispatch a command to its appropriate handler.
     *
     * @param  mixed  $command
     * @return mixed
     */
    public function dispatch($command)
   {
        if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
            return $this->dispatchToQueue($command);
        }

        return $this->dispatchNow($command);
    }

這裡的 commandShouldBeQueued 方法點進去看下。


/**
     * Determine if the given command should be queued.
     *
     * @param  mixed  $command
     * @return bool
     */
    protected function commandShouldBeQueued($command)
{       
      return $command instanceof ShouldQueue;
    }

這裡就判斷任務類是否屬於 ShouldQueue 的例項,因為開頭我們建立的類是繼承自此類的。繼承此類表示我們的佇列是非同步執行而非同步。

首先 Laravel 會去檢查任務中是否設定了 connection 屬性,表示的是把此次任務傳送到哪個連線中,如果未設定,使用預設的。通過設定的連線,使用一個 queueResolver 的閉包來構建應該使用哪一個佇列驅動的例項。這裡我並沒有在任務類中設定指定的 $connetction ,所以會使用預設配置,我在一開始就配置 redis,列印一下這個$queue,將得到一個Illuminate\Queue\RedisQueue 的例項。直接看最後一句。 

推送至指定佇列

    /**
     * Push the command onto the given queue instance.
     *
     * @param  \Illuminate\Contracts\Queue\Queue  $queue
     * @param  mixed  $command
     * @return mixed
     */
    protected function pushCommandToQueue($queue, $command)
  {
        if (isset($command->queue, $command->delay)) {
            return $queue->laterOn($command->queue, $command->delay, $command);
        }

        if (isset($command->queue)) {
            return $queue->pushOn($command->queue, $command);
        }

        if (isset($command->delay)) {
            return $queue->later($command->delay, $command);
        }

        return $queue->push($command);
    }

這個函式的作用就是將任務推送到給定的佇列中,這其中會根據任務類的配置,執行對應的操作。比如第一句,延遲將任務推送到佇列。這裡我們的任務類什麼都沒配置,當然直接追蹤最後一句。前面已經說了,這裡我們得到的是一個 Illuminate\Queue\RedisQueue 的例項,那就直接去訪問這個類中的 push方法吧。

    /**
     * Push a new job onto the queue.
     *
     * @param  object|string  $job
     * @param  mixed   $data
     * @param  string  $queue
     * @return mixed
     */
    public function push($job, $data = '', $queue = null)
  {
        return $this->pushRaw($this->createPayload($job, $data), $queue);
    }

    /**
     * Push a raw payload onto the queue.
     *
     * @param  string  $payload
     * @param  string  $queue
     * @param  array   $options
     * @return mixed
     */
    public function pushRaw($payload, $queue = null, array $options = [])
{
        $this->getConnection()->rpush($this->getQueue($queue), $payload);
        return json_decode($payload, true)['id'] ?? null;
    }

就一句話推送任務到佇列去,繼續追蹤。下面的方法。可以看到 Laravelredis 為佇列是通過 List 的資料形式存在的,每推送一個任務,從左往右排隊進入列表中,鍵名,夠清晰了吧,因為我們並沒有設定 $queue,所以取預設值,那麼我們得到的就是一個 queues:default 的字串。

    /**
     * Get the queue or return the default.
     *
     * @param  string|null  $queue
     * @return string
     */
    public function getQueue($queue)
   {
        return 'queues:'.($queue ?: $this->default);
    }

至於值嘛,我們也可以看下,一波對於是否是物件的判斷之後,通過 json_encode() 進行編碼。

    protected function createPayload($job, $data = '')
  {
        $payload = json_encode($this->createPayloadArray($job, $data));

        if (JSON_ERROR_NONE !== json_last_error()) {
            throw new InvalidPayloadException(
                'Unable to JSON encode payload. Error code: '.json_last_error()
            );
        }

        return $payload;
    }

    /**
     * Create a payload array from the given job and data.
     *
     * @param  string  $job
     * @param  mixed   $data
     * @return array
     */
    protected function createPayloadArray($job, $data = '')
  {
        return is_object($job)
                    ? $this->createObjectPayload($job)
                    : $this->createStringPayload($job, $data);
    }

結尾

到這裡的話程式碼已經追蹤的差不多了,當然這裡面還有很多是沒有提到的,比如,在執行 queue:work 之後,底層都在做什麼。佇列任務是如何並取出來的,work 還可以跟很多的引數,這裡面都發生了什麼。,延遲任務,監聽機制.....,我覺得看原始碼雖然一開始頭頂略微涼了一點,但是看多了,你就是行走中的移動文件。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

吳親庫裡

相關文章