“我總是控制不好自己的情緒。其實,情緒只是對自己無能的憤怒罷了。”
開篇
日常開發使用佇列的場景不少了吧,至於如何使用,我想文件已經寫的很清楚了,畢業一年多了,七月份換一家新公司的時候開始使用 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();
}
重新回到 Application
的 registerConfiguredProviders
方法。
/**
* 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.php
的 providers
裡面的陣列配置。
'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;
}
就一句話推送任務到佇列去,繼續追蹤。下面的方法。可以看到 Laravel
以 redis
為佇列是通過 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 協議》,轉載必須註明作者和本文連結