laravel綜合話題隊列——異步消息的定義
由上篇laravel綜合話題:隊列——異步消息的定義_隊列,php_szuaudi的博客-CSDN博客
我們知道,laravel通過調用dispatch
方法分發任務,但實際上整個過程只是做異步消息的定義工作。在本篇中,我們探究任務類對象是怎麼被持久化的。
任務的分發
一個非常關鍵的地方是PendingDispatch
類。在該類中有一個不能忽略的方法:
/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
app(Dispatcher::class)->dispatch($this->job);
}
在PendingDispatch
對象銷燬時(可能是HTTP請求結束,腳本執行完成),將會執行該段代碼,將會把我們定義的任務類對象傳遞給app(Dispatcher::class)->dispatch
方法。我們看一下app(Dispatcher::class)->dispatch($this->job);
的內容。
Dispatcher
Illuminate\Contracts\Bus\Dispatcher
是一個laravel的Contracts
接口,接口的實現類在laravel應用程序啓動時創建及綁定。參見laravel中文文檔laravel核心架構中的說明:Contracts |《Laravel 5.5 中文文檔 5.5》| Laravel China 社區。
我們使用php artisan tinker
在控制檯中打印出Dispatcher
綁定的類。
可以發現Dispatcher
接口綁定的是Illuminate\Bus\Dispatcher
類,我們在Illuminate\Bus\Dispatcher
類中查看Dispatcher
方法做了什麼。
<?php
class Dispatcher implements QueueingDispatcher
{
...
/**
* Create a new command dispatcher instance.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param \Closure|null $queueResolver
* @return void
*/
public function __construct(Container $container, Closure $queueResolver = null)
{
$this->container = $container;
$this->queueResolver = $queueResolver;
$this->pipeline = new Pipeline($container);
}
/**
* 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);
}
/**
* Dispatch a command to its appropriate handler in the current process.
*
* @param mixed $command
* @param mixed $handler
* @return mixed
*/
public function dispatchNow($command, $handler = null)
{
if ($handler || $handler = $this->getCommandHandler($command)) {
$callback = function ($command) use ($handler) {
return $handler->handle($command);
};
} else {
$callback = function ($command) {
return $this->container->call([$command, 'handle']);
};
}
return $this->pipeline->send($command)->through($this->pipes)->then($callback);
}
/**
* Determine if the given command should be queued.
*
* @param mixed $command
* @return bool
*/
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
/**
* Dispatch a command to its appropriate handler behind a queue.
*
* @param mixed $command
* @return mixed
*
* @throws \RuntimeException
*/
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
if (! $queue instanceof Queue) {
throw new RuntimeException('Queue resolver did not return a Queue implementation.');
}
if (method_exists($command, 'queue')) {
return $command->queue($queue, $command);
}
return $this->pushCommandToQueue($queue, $command);
}
/**
* 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);
}
}
現在我們梳理一下大概過程:
dispatch
方法被調用。如果$this->queueResolver && $this->commandShouldBeQueued($command)
都不爲false,執行dispatchToQueue
方法;否則執行dispatchNow
方法。
從方法名上我們大概知道:如果隊列解析器和命令應該被放入隊列中就分發到隊列中;否則就現在就分發。commandShouldBeQueued
方法被調用。方法返回$command instanceof ShouldQueue
,即如果我們定義的任務類對象是ShouldQueue
的子類,就是說任務類implement
了ShouldQueue
接口就返回true。dispatchToQueue
方法被調用。$queue = call_user_func($this->queueResolver, $connection);
生成一個隊列實例$queue
,在隊列實例上調用queue
或pushCommandToQueue
方法。- 由於我們任務類中並沒有定義
queue
方法,pushCommandToQueue
方法被調用。
根據任務類的配置,執行laterOn
,pushOn
,later
,push
方法。最後任務類對象被髮送給隊列。
我們再來看看dispatchNow
方法。在dispatchNow
方法中,經過管道處理,現在laravel容器中查看是否有綁定的處理器,如果有直接調用處理器的handler
方法;否則通過容器調用任務類的handler
方法。
接下來我們來探究$queue = call_user_func($this->queueResolver, $connection);
的執行過程。
queueResolver
是Dispatcher
的一個屬性,並在__construct
構造方法中初始化。
看一下Dispatcher
實例化的過程:
<?php
namespace Illuminate\Bus;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Bus\Dispatcher as DispatcherContract;
use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;
use Illuminate\Contracts\Bus\QueueingDispatcher as QueueingDispatcherContract;
class BusServiceProvider extends ServiceProvider
{
...
/**
* 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
);
}
}
Dispatcher
在BusServiceProvider
服務提供者中被註冊。關於服務提供者更多信息參見:https://learnku.com/docs/laravel/5.5/providers/1290。可以看出Dispatcher
的queueResolver
屬性是一個閉包函數。代碼call_user_func($this->queueResolver, $connection)
調用了這個閉包函數,函數返回$app[QueueFactoryContract::class]->connection($connection);
生成的返回值。
我們通過php artisan tinker
在控制檯中打印出Illuminate\Contracts\Queue\Factory
綁定的類:
QueueManager
在laravel核心構架——DB Facade_szuaudi的博客-CSDN博客一文中,我們發現對DB
類上的操作引用了DataManager
類,且在DataManager
類中的connection
方法可以返回Connection
類型的對象。QueueManager
具有類似的操作。在Facades |《Laravel 5.5 中文文檔 5.5》| Laravel China 社區提供了使用Facade使用QueueManager
對象的方式。
<?php
class QueueManager implements FactoryContract, MonitorContract
{
...
/**
* Resolve a queue connection instance.
*
* @param string $name
* @return \Illuminate\Contracts\Queue\Queue
*/
public function connection($name = null)
{
$name = $name ?: $this->getDefaultDriver();
// If the connection has not been resolved yet we will resolve it now as all
// of the connections are resolved when they are actually needed so we do
// not make any unnecessary connection to the various queue end-points.
if (! isset($this->connections[$name])) {
$this->connections[$name] = $this->resolve($name);
$this->connections[$name]->setContainer($this->app);
}
return $this->connections[$name];
}
/**
* Resolve a queue connection.
*
* @param string $name
* @return \Illuminate\Contracts\Queue\Queue
*/
protected function resolve($name)
{
$config = $this->getConfig($name);
return $this->getConnector($config['driver'])
->connect($config)
->setConnectionName($name);
}
/**
* Get the connector for a given driver.
*
* @param string $driver
* @return \Illuminate\Queue\Connectors\ConnectorInterface
*
* @throws \InvalidArgumentException
*/
protected function getConnector($driver)
{
if (! isset($this->connectors[$driver])) {
throw new InvalidArgumentException("No connector for [$driver]");
}
return call_user_func($this->connectors[$driver]);
}
/**
* Add a queue connection resolver.
*
* @param string $driver
* @param \Closure $resolver
* @return void
*/
public function addConnector($driver, Closure $resolver)
{
$this->connectors[$driver] = $resolver;
}
/**
* Dynamically pass calls to the default connection.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->connection()->$method(...$parameters);
}
...
}
QueueManager
對象的connection
方法返回一個connections
數組中的元素。在返回connections[$name]
之前會檢查是否存在這個鏈接,如果不存在會調用resolver
方法創建連接。
在resolver
方法中調用getConnector
方法獲取連接器,後把連接配置$config
傳給connection
方法進行連接,連接後調用setConnectionName
方法設置連接名稱。這裏的$name
就是在connection
方法中的連接名,對應着config/queue.php
配置文件的connections
的下標。
getConnector
返回的是什麼類型呢?
在getConnector
中,使用call_user_func
方法調用$this->connectors[$driver]
,這裏的$driver
對應着config/queue.php
配置連接中的driver
的值。一般會有redis、sync、database、beanstalkd
等驅動,對應着不同的存儲系統。
$this->connectors
由addConnector
方法添加。addConnector
方法的參數有$driver
和$resolver
,$driver
對應着驅動的名稱,$resolver
是一個閉包函數。
同樣的,我們可以在其對應的服務提供者中找到QueueManager
的初始化。
<?php
namespace Illuminate\Queue;
class QueueServiceProvider extends ServiceProvider
{
...
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerManager();
$this->registerConnection();
$this->registerWorker();
$this->registerListener();
$this->registerFailedJobServices();
$this->registerOpisSecurityKey();
}
/**
* Register the queue manager.
*
* @return void
*/
protected function registerManager()
{
$this->app->singleton('queue', function ($app) {
// Once we have an instance of the queue manager, we will register the various
// resolvers for the queue connectors. These connectors are responsible for
// creating the classes that accept queue configs and instantiate queues.
return tap(new QueueManager($app), function ($manager) {
$this->registerConnectors($manager);
});
});
}
/**
* Register the default queue connection binding.
*
* @return void
*/
protected function registerConnection()
{
$this->app->singleton('queue.connection', function ($app) {
return $app['queue']->connection();
});
}
/**
* Register the connectors on the queue manager.
*
* @param \Illuminate\Queue\QueueManager $manager
* @return void
*/
public function registerConnectors($manager)
{
foreach (['Null', 'Sync', 'Database', 'Redis', 'Beanstalkd', 'Sqs'] as $connector) {
$this->{"register{$connector}Connector"}($manager);
}
}
/**
* Register the Null queue connector.
*
* @param \Illuminate\Queue\QueueManager $manager
* @return void
*/
protected function registerNullConnector($manager)
{
$manager->addConnector('null', function () {
return new NullConnector;
});
}
/**
* Register the Sync queue connector.
*
* @param \Illuminate\Queue\QueueManager $manager
* @return void
*/
protected function registerSyncConnector($manager)
{
$manager->addConnector('sync', function () {
return new SyncConnector;
});
}
/**
* Register the database queue connector.
*
* @param \Illuminate\Queue\QueueManager $manager
* @return void
*/
protected function registerDatabaseConnector($manager)
{
$manager->addConnector('database', function () {
return new DatabaseConnector($this->app['db']);
});
}
/**
* Register the Redis queue connector.
*
* @param \Illuminate\Queue\QueueManager $manager
* @return void
*/
protected function registerRedisConnector($manager)
{
$manager->addConnector('redis', function () {
return new RedisConnector($this->app['redis']);
});
}
/**
* Register the Beanstalkd queue connector.
*
* @param \Illuminate\Queue\QueueManager $manager
* @return void
*/
protected function registerBeanstalkdConnector($manager)
{
$manager->addConnector('beanstalkd', function () {
return new BeanstalkdConnector;
});
}
/**
* Register the Amazon SQS queue connector.
*
* @param \Illuminate\Queue\QueueManager $manager
* @return void
*/
protected function registerSqsConnector($manager)
{
$manager->addConnector('sqs', function () {
return new SqsConnector;
});
}
...
}
在QueueServiceProvider
中,可以發現,對'Null', 'Sync', 'Database', 'Redis', 'Beanstalkd', 'Sqs'
不同的驅動,分別創建了NullConnector,SyncConnector,DatabaseConnector,RedisConnector,BeanstalkdConnector,SqsConnector
類型的對象。
QueueManager
中connectors
保持就是這些對象,connection
及setConnectionName
方法是在這些對象上進行調用的。
如果我使用Redis服務器做驅動,那麼我使用的隊列連接器就是RedisConnector
。
下面我們以RedisConnector
類爲例,觀察connect
方法:
<?php
namespace Illuminate\Queue\Connectors;
class RedisConnector implements ConnectorInterface
{
...
/**
* Establish a queue connection.
*
* @param array $config
* @return \Illuminate\Contracts\Queue\Queue
*/
public function connect(array $config)
{
return new RedisQueue(
$this->redis, $config['queue'],
$config['connection'] ?? $this->connection,
$config['retry_after'] ?? 60
);
}
...
}
connect
方法返回了一個RedisQueue
的示例。所以,最終QueueManager
的connection
方法返回的是RedisQueue
的類對象。
回到最初的的Dispatcher
類:
// 隊列
$queue = call_user_func($this->queueResolver, $connection);
// queueResolver
$this->app->singleton(Dispatcher::class, function ($app) {
return new Dispatcher($app, function ($connection = null) use ($app) {
return $app[QueueFactoryContract::class]->connection($connection);
});
});
// 保持任務
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);
}
由此可以知,最終我們定義的任務類通過被傳遞給了隊列。如果我使用Redis存儲服務器做驅動,那對應的連接者就是RedisQueue
。
接下來,我們探究RedisQueue
的類push
方法又做了些什麼。
Queue/RedisQueue
我們來看幾個重要的方法。
<?php
namespace Illuminate\Queue;
class RedisQueue extends Queue implements QueueContract
{
...
/**
* Get the size of the queue.
*
* @param string $queue
* @return int
*/
public function size($queue = null)
{
$queue = $this->getQueue($queue);
return $this->getConnection()->eval(
LuaScripts::size(), 3, $queue, $queue.':delayed', $queue.':reserved'
);
}
/**
* 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;
}
/**
* Create a payload string from the given job and data.
*
* @param string $job
* @param mixed $data
* @return string
*/
protected function createPayloadArray($job, $data = '')
{
return array_merge(parent::createPayloadArray($job, $data), [
'id' => $this->getRandomId(),
'attempts' => 0,
]);
}
/**
* Get the queue or return the default.
*
* @param string|null $queue
* @return string
*/
public function getQueue($queue)
{
return 'queues:'.($queue ?: $this->default);
}
/**
* Get the connection for the queue.
*
* @return \Illuminate\Redis\Connections\Connection
*/
protected function getConnection()
{
return $this->redis->connection($this->connection);
}
/**
* Get the underlying Redis instance.
*
* @return \Illuminate\Contracts\Redis\Factory
*/
public function getRedis()
{
return $this->redis;
}
}
<?php
namespace Illuminate\Queue;
abstract class Queue
{
/**
* Create a payload string from the given job and data.
*
* @param string $job
* @param mixed $data
* @return string
*
* @throws \Illuminate\Queue\InvalidPayloadException
*/
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);
}
/**
* Create a payload for an object-based queue handler.
*
* @param mixed $job
* @return array
*/
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),
],
];
}
/**
* Get the display name for the given job.
*
* @param mixed $job
* @return string
*/
protected function getDisplayName($job)
{
return method_exists($job, 'displayName')
? $job->displayName() : get_class($job);
}
/**
* Get the expiration timestamp for an object-based queue handler.
*
* @param mixed $job
* @return mixed
*/
public function getJobExpiration($job)
{
if (! method_exists($job, 'retryUntil') && ! isset($job->timeoutAt)) {
return;
}
$expiration = $job->timeoutAt ?? $job->retryUntil();
return $expiration instanceof DateTimeInterface
? $expiration->getTimestamp() : $expiration;
}
...
}
我們梳理一下push
方法涉及的方法調用過程:
push
方法被調用。push
方法中調用createPayload
方法創建載荷;調用pushRaw
方法。- 父類
Queue
的createPayload
方法被調用。createPayload
方法中,調用createPayloadArray
方法生成數組格式的負載。 - 子類
RedisQueue
的createPayloadArray
方法覆蓋了父類Queue
的createPayloadArray
方法。子類createPayloadArray
方法中,通過parent::createPayloadArray($job, $data)
調用父類該方法。 - 父類
createPayloadArray
方法被調用。通過is_object($job)
判斷,調用createObjectPayload
或createStringPayload
。在這裏,$job
是我們定義的任務類對象,createObjectPayload
方法被調用。 - 在父類
createObjectPayload
方法中,返回一個數據:
[
'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),
]
]
數組中,data.command
中保持了經過序列化後的任務類對象。
- 在
RedisQueue
類的createPayloadArray
中在數組中添加了id
和attempts
。 Queue
類的createPayload
繼續執行,使用json_encode
函數,返回數據對應的son字符串。RedisQueue
的push
方法繼續執行,調用pushRaw
方法。pushRaw
方法被調用,在$this->getConnection()
返回的對象上調用rpush
方法,方法中調用$this->getQueue()
方法。getQueue()
方法被調用。方法返回由'queues:'.($queue ?: $this->default)
組成的字符串。這裏的$queue
就是我們傳遞的隊列名稱。getConnection
方法被調用。rpush
方法被調用。
在這裏我們可以知道,我們定義的任務類對象被序列化後組裝在一個數組中,數據通過json_encode
方法被轉爲json字符串。最後被傳遞到getConnection
方法生成對象的rpush
方法中。
我們看getConnection
方法的內部,是在redis
屬性上調用connection
方法。我們運行getRedis
方法,查看redis屬性的值:
redis屬性指向的是一個RedisManager
類的對象。至此我們已經見到了DataManager,QueueManger
,現在又出現了RedisManager
。
類比一下,我想到了另一中獲取RedisManager
的方法:
至此,關於隊列的操作結束了,要想繼續探索下去,可能更多是與Redis有關的內容。更Redis有關的內容,放到另一篇中再探索。
結論
到此,我們知道了哪些更多內容?
- 任務的分發,是從
PendingDispatcher
類的析構方法開始的。 - 任務的最終通過
serialize
方法被存放在數組中,然後整個數據被轉爲json對象進行持久化。 - 數組中有
displayName
下標指示存儲的名稱。默認使用類名作爲任務的名稱,我們可以在任務中添加displayName
方法進行指定。 - 數組中有
timeoutAt
下標指示任務過期時間。默認爲空,我們可以定義retryUntil
方法來指定。 - 數組中有
timeout
下標指示任務超時時間。默認爲空,我們可以在任務類中定義timeout
屬性指定。 - 數組中有
maxTries
下標指示最大重試次數。默認爲空,我們可以在任務類中定義tries
屬性指定。 Queue
接口聲明瞭size
方法用於返回隊列中的任務數。RedisQueue
類的通過queues:+隊列名
的方式組織存儲的key值,我們可以在redis客戶端使用這個key來查看隊列列表。