laravel综合话题:队列——异步消息的分发

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绑定的类。
Contracts\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);
    }
}

现在我们梳理一下大概过程:

  1. dispatch方法被调用。如果$this->queueResolver && $this->commandShouldBeQueued($command)都不为false,执行dispatchToQueue方法;否则执行dispatchNow方法。
    从方法名上我们大概知道:如果队列解析器和命令应该被放入队列中就分发到队列中;否则就现在就分发。
  2. commandShouldBeQueued方法被调用。方法返回$command instanceof ShouldQueue,即如果我们定义的任务类对象是ShouldQueue的子类,就是说任务类implementShouldQueue接口就返回true。
  3. dispatchToQueue方法被调用。$queue = call_user_func($this->queueResolver, $connection);生成一个队列实例$queue,在队列实例上调用queuepushCommandToQueue方法。
  4. 由于我们任务类中并没有定义queue方法,pushCommandToQueue方法被调用。
    根据任务类的配置,执行laterOn,pushOn,later,push方法。最后任务类对象被发送给队列。

我们再来看看dispatchNow方法。在dispatchNow方法中,经过管道处理,现在laravel容器中查看是否有绑定的处理器,如果有直接调用处理器的handler方法;否则通过容器调用任务类的handler方法。

接下来我们来探究$queue = call_user_func($this->queueResolver, $connection);的执行过程。
queueResolverDispatcher的一个属性,并在__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
        );
    }
}

DispatcherBusServiceProvider服务提供者中被注册。关于服务提供者更多信息参见:https://learnku.com/docs/laravel/5.5/providers/1290。可以看出DispatcherqueueResolver属性是一个闭包函数。代码call_user_func($this->queueResolver, $connection)调用了这个闭包函数,函数返回$app[QueueFactoryContract::class]->connection($connection);生成的返回值。
我们通过php artisan tinker在控制台中打印出Illuminate\Contracts\Queue\Factory绑定的类:
QueueManager

QueueManager

laravel核心构架——DB Facade_szuaudi的博客-CSDN博客一文中,我们发现对DB类上的操作引用了DataManager类,且在DataManager类中的connection方法可以返回Connection类型的对象。QueueManager具有类似的操作。在Facades |《Laravel 5.5 中文文档 5.5》| Laravel China 社区提供了使用Facade使用QueueManager对象的方式。
Facede\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->connectorsaddConnector方法添加。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类型的对象。
QueueManagerconnectors保持就是这些对象,connectionsetConnectionName 方法是在这些对象上进行调用的。
connectors
如果我使用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的示例。所以,最终QueueManagerconnection方法返回的是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方法。
  • 父类QueuecreatePayload方法被调用。createPayload方法中,调用createPayloadArray方法生成数组格式的负载。
  • 子类RedisQueuecreatePayloadArray方法覆盖了父类QueuecreatePayloadArray方法。子类createPayloadArray方法中,通过parent::createPayloadArray($job, $data)调用父类该方法。
  • 父类createPayloadArray方法被调用。通过is_object($job)判断,调用createObjectPayloadcreateStringPayload。在这里, $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中在数组中添加了idattempts
  • Queue类的createPayload继续执行,使用json_encode函数,返回数据对应的son字符串。
  • RedisQueuepush方法继续执行,调用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属性的值:
RedisManager
redis属性指向的是一个RedisManager类的对象。至此我们已经见到了DataManager,QueueManger,现在又出现了RedisManager
类比一下,我想到了另一中获取RedisManager的方法:
RedisManager
至此,关于队列的操作结束了,要想继续探索下去,可能更多是与Redis有关的内容。更Redis有关的内容,放到另一篇中再探索。

结论

到此,我们知道了哪些更多内容?

  • 任务的分发,是从PendingDispatcher类的析构方法开始的。
  • 任务的最终通过serialize方法被存放在数组中,然后整个数据被转为json对象进行持久化。
  • 数组中有displayName下标指示存储的名称。默认使用类名作为任务的名称,我们可以在任务中添加displayName方法进行指定。
  • 数组中有timeoutAt下标指示任务过期时间。默认为空,我们可以定义retryUntil方法来指定。
  • 数组中有timeout下标指示任务超时时间。默认为空,我们可以在任务类中定义timeout属性指定。
  • 数组中有maxTries下标指示最大重试次数。默认为空,我们可以在任务类中定义tries属性指定。
  • Queue接口声明了size方法用于返回队列中的任务数。
  • RedisQueue类的通过queues:+队列名的方式组织存储的key值,我们可以在redis客户端使用这个key来查看队列列表。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章