由上篇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);
的内容。
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
方法做了什么。
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
实例化的过程:
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
绑定的类:
在laravel核心构架——DB Facade_szuaudi的博客-CSDN博客一文中,我们发现对DB
类上的操作引用了DataManager
类,且在DataManager
类中的connection
方法可以返回Connection
类型的对象。QueueManager
具有类似的操作。在Facades |《Laravel 5.5 中文文档 5.5》| Laravel China 社区提供了使用Facade使用QueueManager
对象的方式。
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
的初始化。
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
方法:
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
方法又做了些什么。
我们来看几个重要的方法。
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;
}
}
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来查看队列列表。