Laravel 原始碼閱讀 - Queue

slpi1發表於2019-08-06

1.1 QueueServiceProvider

Illuminate\Queue\QueueServiceProvider

佇列服務由服務提供者QueueServiceProvider註冊。

  • registerManager() 註冊佇列管理器,同時新增 Null/Sync/Database/Redis/Beanstalkd/Sqs 連線驅動
    • Null:不啟動佇列,生產者產生的任務被丟棄
    • Sync:同步佇列,生產者產生的任務直接執行
    • Database:資料庫佇列驅動,生產者產生的任務放入資料庫
    • Redis:Redis佇列驅動,生產者產生的任務放入Redis
    • Beanstalkd:略過
    • Sqs:略過
  • registerConnection() 註冊佇列連線獲取閉包,當需要用到佇列驅動連線時,例項化連線
  • registerWorker() 註冊佇列消費者
  • registerListener() Listen模式註冊佇列消費者
  • registerFailedJobServices() 註冊失敗任務服務
註冊方法 物件 別名
QueueServiceProvider::registerManager() \Illuminate\Queue\QueueManager::class queue
QueueServiceProvider::registerConnection() \Illuminate\Queue\Queue::class queue.connection
QueueServiceProvider::registerWorker() \Illuminate\Queue\Worker::class queue.worker
QueueServiceProvider::registerListener() \Illuminate\Queue\Listener::class queue.listener
QueueServiceProvider::registerFailedJobServices() \Illuminate\Queue\Failed\FailedJobProviderInterface::class queue.failer

1.2 BusServiceProvider

Illuminate\Bus\BusServiceProvider

這個服務提供者註冊了Dispatcher這個服務,可以將具體的任務派發到佇列。

一個可放入佇列的任務類:

<?php

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

class Job implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
}

任務在佇列中需要經過兩個過程:一個是任務入隊,就是將要執行的任務,放到佇列中去的過程,所有會發生這一過程的業務、物件、呼叫等等,可以統稱為生產者;與之對應的,將任務從佇列中取出,並執行的過程,叫做任務出隊,所有會發生這一過程的業務、物件、呼叫等等,可以統稱為消費者。

2.1 任務的要素

2.1.1 Dispatchable

Illuminate\Foundation\Bus\Dispatchable

這個trait給任務新增了兩個靜態方法dispatch/withChain,賦予了任務派發的介面。

  1. dispatch

dispatch方法觸發任務指派動作。當執行 Job::dispatch()時,會例項化一個Illuminate\Foundation\Bus\PendingDispatch物件PendingDispatch,並且將任務呼叫類例項化後的物件job當作建構函式的引數:

// Illuminate\Foundation\Bus\Dispatchable::trait
public static function dispatch()
{
    // 這裡的static轉發到實際執行dispath的類 Job::dispatch,也就是Job類
    return new PendingDispatch(new static(...func_get_args()));
}

PendingDispatch物件接下來可以通過鏈式呼叫來指定佇列相關資訊 onConnection/onQueue/allOnConnection/allOnQueue/delay/chain,然後在解構函式中,做實際的派發動作:

// Illuminate\Foundation\Bus\PendingDispatch::class
public function __destruct()
{
    // Illuminate\Contracts\Bus\Dispatcher
    // 這個服務由Illuminate\Bus\BusServiceProvider註冊
    app(Dispatcher::class)->dispatch($this->job);
}

PendingDispath這個中間指派者的作用,就是引出這裡從容器中解析出來的服務Dispatcher::class,真正的任務指派者Dispatcher

  1. withChain

withChain用於指定應該按順序執行的佇列列表。它只是一個語法糖,實際上的效果等同於:

// 1. withChain的用法
Job::withChain([
    new OptimizePodcast,
    new ReleasePodcast
])->dispatch();

// 2. 等同於dispatch的用法
Job::dispatch()->chain([
    new OptimizePodcast,
    new ReleasePodcast
])

2.1.2 Queueable

Illuminate\Bus\Queueable

上文提到的PendingDispath,可以指定佇列資訊的方法,都是轉發到任務對應的方法進行呼叫,Queueable就是實現了這部分的功能。這部分包括以下介面:

方法名 描述
onConnection 指定連線名
onQueue 指定佇列名
allOnConnection 指定工作鏈的連線名
allOnQueue 指定工作鏈的佇列名
delay 設定延遲執行時間
chain 指定工作鏈

以及最後一個方法dispatchNextJobInChain。上述方法都是在任務執行前呼叫,設定任務相關引數。dispatchNextJobInChain是在任務執行期間,如果檢查到任務定義了工作鏈,就會派發工作鏈上面的任務到佇列中。

2.1.3 SerializesModels

Illuminate\Queue\SerializesModels

這個trait的作用是字串化任務資訊,方便將任務資訊儲存到資料庫或Redis等儲存器中,然後在佇列的消費端取出任務資訊,並據此重新例項化為任務物件,便於執行任務。

2.1.4 InteractsWithQueue

Illuminate\Queue\InteractsWithQueue

這個trait賦予了任務與佇列進行資料互動的能力。InteractsWithQueue是任務的必要組成,如果一個任務只能被執行,而不能與佇列進行互動,那麼這個任務在佇列中的狀態就是未知的,必然會造成混亂。InteractsWithQueue與佇列的互動能力來源於$job屬性,它是一個QueueJob例項,需要與任務的概念進行區別:任務是泛指可執行的物件,而這個$job,是在任務出隊以後,解析出來的QueueJob物件。

即時一個任務類實現了InteractsWithQueue,它在例項化的時候並沒有$job這個屬性。需要等到出隊後的執行過程中,這個$job才被手動設定給任務。

2.1.5 ShouldQueue

Illuminate\Contracts\Queue\ShouldQueue

ShouldQueue也是是任務的必要實現的介面。只有實現了ShouldQueue介面的任務,才可以被放入佇列。上文所提到的真正的任務指派者Dispatcher,它在PendingDispath銷燬時所執行的dispatch方法程式碼如下:

// Illuminate\Bus\Dispatcher::class
public function dispatch($command)
{
    // 檢查任務指派者是否注入了佇列服務
    // 並且當前任務需要方法佇列
    if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
        return $this->dispatchToQueue($command);
    }

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

commandShouldBeQueued方法的作用就是檢查任務物件是否實現了ShouldQueue介面。如果是,就是執行dispatchToQueue方法,將任務放入佇列之中;否則執行dispatchNow,放入佇列執行棧(pipeline),進行同步執行。

2.2 任務入隊

任務是如何被放入佇列中的呢?這就引出了payload這個概念。當Dispatcher這個服務通過dispatch方法派發任務時,會通過佇列服務,將任務push到佇列中,在push的過程中會執行createPayloadArray方法:

// Illuminate\Queue\Queue

protected function createPayloadArray($job, $data = '')
{
    // 如果我們文中所提到的“任務”,是一個物件的話,會呼叫createObjectPayload方法
    // 進行一次封裝,將封裝後的資料Payload存入佇列,如果不是一個物件的話,呼叫
    // createStringPayload進行一次封裝,然後存入佇列
    return is_object($job)
                ? $this->createObjectPayload($job)
                : $this->createStringPayload($job, $data);
}

protected function createObjectPayload($job)
{
    return [
        'displayName' => $this->getDisplayName($job),
        'job' => 'Illuminate\Queue\CallQueuedHandler@call',
        'maxTries' => $job->tries ?? null,
        'timeout' => $job->timeout ?? null,
        'timeoutAt' => $this->getJobExpiration($job),
        'data' => [
            'commandName' => get_class($job),
            'command' => serialize(clone $job),
        ],
    ];
}

protected function createStringPayload($job, $data)
{
    return [
        'displayName' => is_string($job) ? explode('@', $job)[0] : null,
        'job' => $job, 'maxTries' => null,
        'timeout' => null, 'data' => $data,
    ];
}

createObjectPayload/createStringPayload 這兩個方法return的陣列就是payload,是便於儲存的一種格式。請特別注意 Illuminate\Queue\CallQueuedHandler@call 這部分出現的CallQueuedHandler這個物件,他是任務機制中的重要一環。

2.3 任務出隊

任務出隊是建立在消費者開始工作的基礎之上的。在laravel的應用中,一類消費者就是Worker,佇列處理器。通過命令列php artisan queue:work來啟動一個Worker。Worker在daemon模式下,會不斷的嘗試從佇列中取出任務並執行,這一過程有以下執行環節:

  • 第一步:檢查是否要暫停佇列,是則暫停一段時間,否則經行下一步
  • 第二步:取出當前要執行的任務,並給任務設定一個超時程式,
  • 第三步:執行任務,如果當前沒有任務,暫停一段時間
  • 第四步:檢查是否要停止佇列

遇到三種情況會停止佇列:

  • Worker程式收到SIGTERM訊號
  • 使用記憶體超過限制
  • 收到重啟命令

第二步就是任務出隊的過程。

// 該方法表示,從連線$connection中,名稱為$queue的佇列中取出下一個任務。
protected function getNextJob($connection, $queue)
{
    try {
        foreach (explode(',', $queue) as $queue) {
            if (! is_null($job = $connection->pop($queue))) {
                return $job;
            }
        }
    } catch (Exception $e) {
        $this->exceptions->report($e);

        $this->stopWorkerIfLostConnection($e);
    } catch (Throwable $e) {
        $this->exceptions->report($e = new FatalThrowableError($e));

        $this->stopWorkerIfLostConnection($e);
    }
}

如果佇列驅動使用的時Database,那麼$connection指的就是Illuminate\Queue\DatabaseQueue的例項,如果佇列驅動使用的時Redis,那麼$connection指的就是Illuminate\Queue\RedisQueue的例項。

$connection的pop方法會從佇列儲存中取出下一個payload,經過佇列驅動的轉化,得到不同的QueueJob例項,也就是上文提到的$job物件,呼叫$job的fire方法,任務就開始執行。

2.4 任務執行 - CallQueuedHandler

CallQueuedHandler就像是佇列這個軌道上的一輛車,是任務機制中的重要環節。

我們可以把入隊與出隊稱為佇列的“內部操作”,他們是屬於Queue這個概念之內的問題。而CallQueuedHandler可以看成Queue與外部任務對接的“標準介面”,如果把所有要執行的任務稱為“可執行物件”,那麼,只需要用CallQueueHandle這個物件來裝載“可執行物件”,就可以讓這個“可執行物件”利用佇列的機制來執行。

在入隊時,CallQueueHandle與“可執行物件”組合成為payload。在出隊時,payload重放成為QueueJob,QueueJob呼叫fire的下一個環節,就是CallQueueHandle。

2.4.1 CallQueuedHandler的作用

在入隊與出隊的過程中,CallQueuedHandler並不發生任何作用,他只是隨payload在佇列的儲存中流轉進出。當任務被取出執行時,CallQueuedHandler就開始發揮作用。CallQueuedHandler的作用可以歸納為兩點:

  • 繼承QueueJob呼叫的fire方法,轉發到CallQueuedHandler的handle方法,然後啟動任務的執行方法。
  • 處理任務執行結果與佇列中資料(payload)的去留關係

2.4.2 任務執行的呼叫棧

QueueJob::fire() ->  CallQueuedHandler::handle() ->  [任務或其他可執行物件呼叫]
// worker->process()  消費者執行一個任務
public function process($connectionName, $job, WorkerOptions $options)
{
    try {
        $this->raiseBeforeJobEvent($connectionName, $job);

        $this->markJobAsFailedIfAlreadyExceedsMaxAttempts(
            $connectionName, $job, (int) $options->maxTries
        );

        $job->fire();

        $this->raiseAfterJobEvent($connectionName, $job);
    } catch (Exception $e) {
        $this->handleJobException($connectionName, $job, $options, $e);
    } catch (Throwable $e) {
        $this->handleJobException(
            $connectionName, $job, $options, new FatalThrowableError($e)
        );
    }
}

// Job->fire()
public function fire()
{
    $payload = $this->payload();

    // 解析佇列任務資訊,檢視上文中的createPayload方法:  $payload = ['job' => 'Illuminate\Queue\CallQueuedHandler@call']
    // 所以這裡$class = Illuminate\Queue\CallQueuedHandler, $method = call
    list($class, $method) = JobName::parse($payload['job']);

    // 如果payload是通過CallQueuedHandler進行包裝的,那麼此時instance就是CallQueuedHandler的例項,method就是call方法
    // 如果payload是通過字串進行包裝的,那麼此時的instance就是制定的任務物件,method就是制定的呼叫方法
    ($this->instance = $this->resolve($class))->{$method}($this, $payload['data']);
}

2.5 小結

laravel提供的隊裡機制的執行呼叫棧,就是上述過程。當我們指佇列的任務機制時,包含的內容有以下兩點:

  • 佇列底層提供的入隊與出隊機制
  • 任務出隊後的執行呼叫棧

在充分理解任務機制的前提下,事件機制就很好理解了。事件監聽器的原理是,通過Illuminate\Events\CallQueuedListener 來作為一個特殊的“任務”,將事件繫結與監聽資訊儲存到這個“任務”中,當事件被觸發時,通過事件解析出與之對應的“任務”,然後對這個“任務”進行派發,執行這個“任務”時,再去執行事件監聽器。所以,這個環節的重點其實是,事件、監聽器、CallQueuedListener三者之間是如何進行關聯的,也就是事件監聽機制。所以我們後面在分析laravel事件機制相關原始碼時,遇到CallQueuedListener這個物件時就知道,這是要開始與佇列進行對接了。

// 1. 觸發一個事件
event(new Event);

// 2. 從觸發事件到進入佇列的過程形容如下
$eventCommand = new \Illuminate\Events\CallQueuedListener(new Event);

new PendingDispatch($eventCommand);

訊息機制的實現與事件機制類似。通過Illuminate\Notifications\SendQueuedNotifications 來作為一個特殊的“任務”,與訊息相關資訊進行關聯,通過SendQueuedNotifications物件來完成入隊與出隊相關過程,然後在執行“任務”SendQueuedNotifications的時候解析出關聯的notifiables和notification,然後據此執行訊息相關邏輯。

上面提到的都是系統提供的佇列機制,除此之外,你還可以手動推送任務到佇列,即通過Queue Facade來指派任務。

// MyTask
class MyTask implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle($job, $args)
    {
        echo "MyTask";
        return true;
    }
}

// 推送至佇列
Queue::push('MyTask@handle', $args, $queueName);

// MyAnotherTask
class MyAnotherTask implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle($args)
    {
        echo "MyTask";
        return true;
    }
}

// 推送至佇列
Queue::push(new MyAnotherTask, $args, $queueName);

5.1 入隊物件是一個例項

通過例項的方式,將任務推送到佇列中,在createPayload的環節,執行的是createObjectPayload方法,這時可以利用系統提供的佇列機制,例項只需要有一個handle方法作為執行方法,來承接CallQueuedHandler::handle()傳遞的呼叫棧。此時,handle方法只需要業務本身涉及到的資料作為引數。

5.2 入隊物件是一個字串

通過字串的方式,將任務推送到佇列中,在createPayload的環節,執行的是createStringPayload方法,這時無法利用系統體統的佇列機制中的第二層內容:執行呼叫棧,在QueueJob::fire()之後會呼叫字串指定的物件及方法。此時,方法除了需要業務本身涉及的資料作為引數外,還需要任務重放得到的QueueJob物件,作為第一個引數,所以上面程式碼中兩個自定義任務的函式簽名是不同的。

如果僅僅通過上述程式碼來執行的話,MyAnotherTask這個任務可能會一直執行下去,原因是缺少對執行任務後的處理:如果任務執行成功,因該從佇列中刪除掉;如果執行失敗,也要有對應的處理措施。也就是CallQueuedHandler的第二個作用。我們不妨來看看CallQueuedHandler,是如何來處理這個問題的。

無論是系統的任務機制,或是事件機制,消費端從佇列中取出任務資訊後,還原出一個Job物件(RedisJob/DatabaseJob),然後執行這個Job的fire方法時,都會藉助CallQueuedHandler這個物件來執行任務的具體內容:

// CallQueuedHandler->call()
public function call(Job $job, array $data)
{
    // 預處理任務資訊
    try {
        $command = $this->setJobInstanceIfNecessary(
            $job, unserialize($data['command'])
        );
    } catch (ModelNotFoundException $e) {
        return $this->handleModelNotFound($job, $e);
    }

    // 通過dispatcher同步執行任務
    $this->dispatcher->dispatchNow(
        $command, $this->resolveHandler($job, $command)
    );

    // 如果任務未失敗,且未釋放,確保工作鏈上的任務都已派發
    if (!$job->hasFailed() && !$job->isReleased()) {
        $this->ensureNextJobInChainIsDispatched($command);
    }

    // 如果任務未刪除或未釋放,刪除任務
    if (!$job->isDeletedOrReleased()) {
        $job->delete();
    }
}

也就是說,如果通過字串的方式,手動派發任務到佇列,需要自己手動進行像CallQueuedHandler::call()方法中那樣的收尾工作,使任務執行完畢後,清除任務儲存在佇列中的資訊,避免任務被重新執行。

常用的資料儲存驅動是Database與Redis,我們以Redis作為例子來做說明。

6.1 Redis

假設我們現在設定有一個名叫queue的佇列,那麼,在佇列執行的過程中,會有下列幾個key被redis用到:

  • queue 任務資訊預設儲存的key
  • queue:reserved 任務執行過程中,臨時儲存的key
  • queue:delayed 任務執行失敗,被重新發布到的key,或者延遲執行的任務被髮布到的key

6.1.1 入隊

任務資訊被推入佇列時,呼叫RedisQueue的push方法,將任務資訊的載體payload,rpush到鍵名為queue的lists中:

    /**
     * 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;
    }

如果是將一個任務推入佇列中延遲執行,呼叫的是RedisQueue的later方法,zadd到鍵名為queue:delayed的zset中,延遲時長作為排序的依據:


    /**
     * Push a new job onto the queue after a delay.
     *
     * @param  \DateTimeInterface|\DateInterval|int  $delay
     * @param  object|string  $job
     * @param  mixed   $data
     * @param  string  $queue
     * @return mixed
     */
    public function later($delay, $job, $data = '', $queue = null)
    {
        return $this->laterRaw($delay, $this->createPayload($job, $data), $queue);
    }

    /**
     * Push a raw job onto the queue after a delay.
     *
     * @param  \DateTimeInterface|\DateInterval|int  $delay
     * @param  string  $payload
     * @param  string  $queue
     * @return mixed
     */
    protected function laterRaw($delay, $payload, $queue = null)
    {
        $this->getConnection()->zadd(
            $this->getQueue($queue).':delayed', $this->availableAt($delay), $payload
        );

        return json_decode($payload, true)['id'] ?? null;
    }

6.1.2 出隊

出隊的方法只有一個,就是RedisQueue的pop方法。

    /**
     * Pop the next job off of the queue.
     *
     * @param  string  $queue
     * @return \Illuminate\Contracts\Queue\Job|null
     */
    public function pop($queue = null)
    {
        $this->migrate($prefixed = $this->getQueue($queue));

        list($job, $reserved) = $this->retrieveNextJob($prefixed);

        if ($reserved) {
            return new RedisJob(
                $this->container, $this, $job,
                $reserved, $this->connectionName, $queue ?: $this->default
            );
        }
    }

    /**
     * Migrate any delayed or expired jobs onto the primary queue.
     *
     * @param  string  $queue
     * @return void
     */
    protected function migrate($queue)
    {
        $this->migrateExpiredJobs($queue.':delayed', $queue);

        if (! is_null($this->retryAfter)) {
            $this->migrateExpiredJobs($queue.':reserved', $queue);
        }
    }

    /**
     * Migrate the delayed jobs that are ready to the regular queue.
     *
     * @param  string  $from
     * @param  string  $to
     * @return array
     */
    public function migrateExpiredJobs($from, $to)
    {
        return $this->getConnection()->eval(
            LuaScripts::migrateExpiredJobs(), 2, $from, $to, $this->currentTime()
        );
    }

在出隊之前,先檢查queue:delayed上是否有到期的任務,有的話,先將這部分任務的資訊轉移到queue上,如果設定有超時時間,還會檢查queue:reserved上是否有到期的任務,將這部分的任務資訊也轉移到queue上。

接著通過retrieveNextJob方法獲取下一個要執行的任務資訊:從queue中取出第一個任務,將他的attempt值加一後放入到queue:reserved中。

    /**
     * Retrieve the next job from the queue.
     *
     * @param  string  $queue
     * @return array
     */
    protected function retrieveNextJob($queue)
    {
        return $this->getConnection()->eval(
            LuaScripts::pop(), 2, $queue, $queue.':reserved',
            $this->availableAt($this->retryAfter)
        );
    }

// LuaScripts::pop
    /**
     * Get the Lua script for popping the next job off of the queue.
     *
     * KEYS[1] - The queue to pop jobs from, for example: queues:foo
     * KEYS[2] - The queue to place reserved jobs on, for example: queues:foo:reserved
     * ARGV[1] - The time at which the reserved job will expire
     *
     * @return string
     */
    public static function pop()
    {
        return <<<'LUA'
-- Pop the first job off of the queue...
local job = redis.call('lpop', KEYS[1])
local reserved = false

if(job ~= false) then
    -- Increment the attempt count and place job on the reserved queue...
    reserved = cjson.decode(job)
    reserved['attempts'] = reserved['attempts'] + 1
    reserved = cjson.encode(reserved)
    redis.call('zadd', KEYS[2], ARGV[1], reserved)
end

return {job, reserved}
LUA;
    }

6.1.3 執行結果

在任務執行成功時,檢查任務是否被刪除或Release,如果沒有的話,就從queue:reserved中刪除任務資訊;如果執行失敗的話,檢查是否超過最大執行次數,超過則刪除任務資訊,否則標記為已刪除,從queue:reserved中刪除任務資訊,並重新發布任務到queue:delayed中。


    /**
     * Delete a reserved job from the reserved queue and release it.
     *
     * @param  string  $queue
     * @param  \Illuminate\Queue\Jobs\RedisJob  $job
     * @param  int  $delay
     * @return void
     */
    public function deleteAndRelease($queue, $job, $delay)
    {
        $queue = $this->getQueue($queue);

        $this->getConnection()->eval(
            LuaScripts::release(), 2, $queue.':delayed', $queue.':reserved',
            $job->getReservedJob(), $this->availableAt($delay)
        );
    }

// LuaScripts::release()
    /**
     * Get the Lua script for releasing reserved jobs.
     *
     * KEYS[1] - The "delayed" queue we release jobs onto, for example: queues:foo:delayed
     * KEYS[2] - The queue the jobs are currently on, for example: queues:foo:reserved
     * ARGV[1] - The raw payload of the job to add to the "delayed" queue
     * ARGV[2] - The UNIX timestamp at which the job should become available
     *
     * @return string
     */
    public static function release()
    {
        return <<<'LUA'
-- Remove the job from the current queue...
redis.call('zrem', KEYS[2], ARGV[1])

-- Add the job onto the "delayed" queue...
redis.call('zadd', KEYS[1], ARGV[2], ARGV[1])

return true
LUA;
    }

6.2 Database

如果佇列驅動是資料庫,這個過程也基本一致,不過只需要用一張資料表來儲存任務資訊,用reserved_at,available_at兩個欄位來表示不同的任務狀態,在取出任務及任務失敗等複雜情況下,通過事務來保證任務執行的結果與資料的一致性。:

  • 給reserved_at欄位賦值,對應Redis中push到queue:reserved
  • 給available_at欄位賦值,對應Redis中push到queue:delayed
  • Redis中LuaScripts指令碼部分的執行,對應資料庫中的事務

關於laravel的佇列就是這些了,具體的細節部分,有大家去針對性的檢視對應原始碼。這裡對整體的邏輯做一個總結,如圖:

Laravel 原始碼閱讀 - Queue

具體payload在Redis中的流轉過程如圖:
Laravel 原始碼閱讀 - Queue

相關文章