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來查看隊列列表。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章