Laravel中的队列以及如何为工作者添加队列的实例

315 阅读8分钟

我们想用一个现实生活中的例子来开始这篇文章。想象一下,一辆冰淇淋车。这是一个非常热的日子,人们想用美味的冰淇淋来提神。买家都在叫卖他们想要的冰淇淋种类;一个买家只想要一份,而另一个买家有一个大家庭,想要一整盒。这种情况似乎并不困难;买家在发送请求,卖家在收钱,准备订单并向顾客提供冰淇淋。但是,如果有很多买家,订单很大,或者几个人同时在冰淇淋车里工作,就可能变得很混乱。许多愤怒的买家可能在等待他们的大订单,而只有一个疯狂的卖家可能因为疲惫而无法完成订单。这可能是发明排队的原因之一。

在这篇文章中, 我们将学习如何使用队列, 定义一个工作者, 并使用不同的情况分析一些例子, 包括如何为一个工作者添加队列, 当我们有几个工作者时,程序将如何表现, 以及如何将任务组合成组。

什么是Laravel中的队列?

我们想使用队列的最常见的情况是重型进程, 例如导入或同步大数据, 这需要更多的时间,而且经常发生。当我们在页面加载时运行一个大的进程, 这意味着用户必须等待,直到该进程完成才能访问内容.如果我们想改善用户体验, 我们可以让这个进程在后台运行, 让用户继续处理其他东西, 当所有的任务完成后, 我们可以通过电子邮件向用户发送通知或消息.

Laravel队列的主要思想是创建PHP方法, 在数据库中注册它们, 然后告诉服务器通过读取数据库来顺序调用这些方法.这个解释也许过于简单了,因为,好在后面会看到,这些方法(或工作)的注册可以更加复杂,如果我们想获得更多关于执行过程的信息,并创建,例如,一个进度条来显示用户他或她的当前状态。

配置和安装队列

我们的安装将以一个artisan命令开始:

php artisan queue:table

这个命令将创建一个迁移文件,包含新的Jobs数据库表的信息。所以,让我们来迁移这个文件:

php artisan migrate

当我们准备好数据库表后,我们可以创建第一个作业。让我们把它命名为CalculateDataJob,通过运行以下命令,我们将创建一个新的PHP文件:

/app/Jobs/CalculateDataJob.php

php artisan make:job CalculateDataJob

我们要运行的任务必须在一个处理的公共方法中描述,比如说下面这个:

public function handle()
{
    for ($x = 1; $x <= 10; $x++) {
        sleep(2);
        // do calculation
    }
}

在这个例子中,我们添加了一个非常小的工作,可以在几毫秒内执行。如果我们想看到更慢的进度,让我们添加一个睡眠函数,将执行时间推迟两秒。handle方法应该控制你的任务的主要逻辑,如果你有更复杂的情况,你可以写一个方法来调用其他方法。

注意: 每次你使用作业并对方法进行修改时,你都需要清理缓存。否则,可能会因为寻找代码中的bug而花费你额外的时间;问题是,服务器正在运行你的旧代码,因为它被缓存了。

php artisan cache:clear

另一个新术语是Dispatching Jobs。它意味着我们需要对这个工作进行排队。让我们创建一个Livewire组件,这是一个简单的按钮,点击一下就可以激活工作。

php artisan make:livewire JobButton

它将创建两个文件:一个类文件/app/Http/Livewire/JobButton.php 和一个视图文件/resources/views/livewire/job-button.blade.php 。在job-button.blade.php ,让我们添加这个简单的代码:

<div wire:click="runJob()">Run job</div>

在你的网站模板的任何地方,插入以下组件标签:<livewire:job-button /> 或刀片指令@livewire('job-button') 。在JobButton.php 文件中,添加这个新方法:

...
use App\Jobs\CalculateDataJob;

public function runJob()
{
    CalculateDataJob::dispatch();
}

此外, 在触发这个方法之前, 我们要配置队列驱动.在Laravel中,有几个选项可以选择:

  • 数据库- 有关工作的信息将被保存在一个数据库表中;
  • Redis- 适合于大型应用和需要更多灵活性的时候;
  • 其他- 三个依赖项, 可以通过使用Composer软件包管理器来安装:Amazon SQS、Beanstalkd或Redis(phpredis PHP扩展)。

对于这个例子,我们将使用数据库作为队列驱动。因此,在.env文件中,让我们找到QUEUE_CONNECTION=sync 行(默认情况下,QUEUE_CONNECTION值是同步的,这意味着我们要立即执行作业),并将其改为QUEUE_CONNECTION=database 。最后,如果我们将点击我们的 "运行作业 "按钮,关于作业的信息将被保存在数据库表 "jobs"。为了处理这个工作,我们只需要在控制台中写一个简单的命令。

php artisan queue:work

运行这个命令后,我们将在控制台中看到有关处理作业的信息。因此,当用户点击一个按钮时,他们不需要在任务完成时等待。该进程将在后台执行。

不管一个进程是由用户还是服务器执行的,我们都可能遇到内存的问题,特别是当我们在处理大数据时。与其执行一个大作业,我们可以将其分成几个小作业。在JobButton.php 文件中,做如下修改:

public function runJob()
{
    for ($x = 1; $x <= 10; $x++) {
        CalculateDataJob::dispatch($x);
    }
}

通过这一改动,我们将通过发送参数来创建作业。因此,在CalculateDataJob.php ,我们需要添加一个新的公共变量,在构造函数中分配它,并在处理方法中使用它:

...

public $x;

public function __construct($x)
{
    $this->x = $x;
}

public function handle()
{
    sleep(2);
    // do calculation with $this->x
}

现在我们可以在数据库中看到十个注册的作业。这些工作是相当小的,以帮助避免服务器内存问题。

Laravel队列工作者

所有这些例子都是在探索Laravel中使用队列的基本原理。当我们运行artisan命令queue:work ,我们激活了工作者。当我们从一个作业变成几个小的作业时, 我们创建了十个独立的工作者, 这意味着这十个作业将被执行, 就像在后台有十个不同的隐形用户。

激活后,队列工作者将无限期地 "活 "在服务器中,并在内存中保存作业信息。因此,当我们改变代码中的某些内容时,我们既要清理缓存,也要重新启动工作者:

php artisan queue:restart

**注意:**例如,在Linux中,关闭控制台窗口后,工作者会被停止。因此,如果我们想在关闭控制台窗口后继续我们的进程,我们需要运行同样的queue:work ,但要有额外的命令。

nohup php artisan queue:work &

如前所述,可以向工作发送参数,将一个大的任务分割成小的任务,例如读取一个大的csv文件并将其分割成小的文件。然而,这种方法的一个主要缺点是,我们需要创建许多小文件,读取数据,然后删除这些文件。解决这个问题的最好方法是读取文件,对数据进行分块,并将分块的数据作为参数发送。这样,数据将被保存在数据库中,当工作者被激活时,数据将从作业表中读取并导入到特定的表中。

队列的其他重要方面是处理错误和通知用户。当我们只有几个作业时,这可能并不关键,但如果我们谈论的是一个大系统,每天执行许多任务,那么登记失败的作业就非常重要。因此, 在同一个CalculateDataJob.php 文件中, 我们可以添加一个新的公共方法:failed.

...
public function failed(Throwable $exception)
{
    // Send user notification of failed job
}

Laravel Queue Workers 进程信息

要在后台运行进程,避免等待,这是一个非常好的解决方案。然而, 如果用户想知道在后台发生了什么呢?最好的办法是呈现实时信息或简单的进度条。从Laravel 8版本开始,我们有了Job Batching。这个功能需要新的数据库表来保存工作的详细信息.

php artisan queue:batches-table
php artisan migrate

因此, 在我们的JobButton.php 文件中, 我们需要将旧的代码改为以下内容:

...
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;

public function runJob()
{
    $this->batchHolder = Bus::batch([])->then(function (Batch $batch) {
        // All jobs completed successfully...
    })->catch(function (Batch $batch, Throwable $e) {
        // First batch job failure detected...
    })->finally(function (Batch $batch) {
        // The batch has finished executing...
    })->name('data_calculation')->dispatch();
    for ($x = 1; $x <= 10; $x++) {
        CalculateDataJob::dispatch($x);
        $this->batchHolder->add(new CalculateDataJob($x));
    }
    return $this->batchHolder;
}

如果我们有不同的作业组,我们可以给它们命名。在这个例子中,是data_calculation,这样会更容易将某一任务的各个作业分开,只接收这些作业的信息。当你使用作业批处理时,在CalculateDataJob.php 文件中添加Batchable

use Illuminate\Bus\Batchable;
...
class ApiUpdateJob implements ShouldQueue
{
    use Batchable,  Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    ...
}

为了创建一个进度条,我们需要在job-button.blade.php 中添加代码:

<div wire:poll.1000ms="checkStatus()">
    <div style="background-color: black; padding: 3px;">
        <div style="background-color: red; text-align:center; height: 20px; width: {{ $loaded_value }}%;">{{ $loaded_value }}%</div>
    </div>
</div>

主html标签将每秒调用checkStatus() 方法并刷新$loaded_value 值。在JobButton.php ,我们将添加一个公共的checkStatus方法。

...
public function checkStatus()
{
    $batches = DB::table('job_batches')->where([['pending_jobs','>',0],['name','=','data_calculation']])->orderBy('created_at', 'desc')->limit(10)->get();
    if(count($batches) > 0){
        $job_status = Bus::findBatch($batches[0]->id)->toArray();
        $this->loaded_value = round($job_status['progress'],0);
    }else{
        $this->loaded_value = 0;
    }
}

在这个方法中,我们将向数据库发送请求,以获得例如最近创建的10个名称为data_calculation的作业,并从第一项中挑选,提供作业状态信息。如果我们打印变量$job_status ,我们将看到5个不同的元素:totalJobs,pendingJobs,processedJobs,progress, andfailedJobs。现在,我们只对进度感兴趣,但如果我们想给用户提供更详细的信息,其他元素也可能非常重要。

额外的例子

队列工作者最流行的用例之一是向用户发送电子邮件信息。这些信息可以是在用户注册后或在用户执行特定操作后发送的欢迎信。首先,将你的邮箱的凭证添加到.env 文件中。如果你不想使用你的私人邮箱,你可以很容易地在mailtrap.io 注册一个免费账户这是非常好的发送和测试电子邮件功能的工具。我们需要创建一个 "可邮寄 "类。

php artisan make:mail SendEmail

它将自动创建一个默认的电子邮件模板app/Mail/SendEmail.php ,可以根据你的需要进行修改。和以前的例子一样,我们需要为此创建一个作业。

php artisan make:job SendWelcomeEmailJob

在新创建的app/Jobs/SendWelcomeEmailJob.php 文件中,需要添加一个处理方法。

use App\Mail\SendEmail;
...
public function handle()
{
    $test_email = 'test@test.com';
    Mail::to($test_email)->send(new SendEmail());
}

最后,我们可以创建一个简单的路由来调用这个作业;在web.php 文件中,添加一个新的路由:send-email

use App\Jobs\SendWelcomeEmailJob;
...
Route::get('send-email', function(){
    dispatch(new SendWelcomeEmailJob());
});

这是另一个我们可以使用队列的好例子。

注意:如果我们想延迟我们的工作进程,我们可以使用延迟方法。

$job = (new SendWelcomeEmailJob())
    ->delay(Carbon::now()->addMinutes(10));
dispatch($job);

应用这个方法将使作业在十分钟后被派发。

实时服务器上的队列工作者

到目前为止,我们所展示的所有例子都应该在本地主机或实时服务器上工作。但是,在实时服务器上,管理员不能经常检查队列工作者是否处于活动状态,并在必要时运行php artisan queue:work 命令,特别是如果项目是国际性的,涉及来自不同时区的用户。当工作者遇到错误或执行超时时会发生什么?您的应用程序队列工作者必须每天24小时都处于活动状态,并且在用户执行特定操作时做出反应。在一个实时服务器上,需要一个进程监视器。这个监控器控制队列工作者,并在进程失败或受到其他进程影响时自动重新启动进程。监督器通常用于Linux服务器。在实时服务器上安装监督器的第一步是运行以下命令。

sudo apt-get install supervisor

安装后,在/etc/supervisor/conf.d 目录中,准备一个配置 laravel-worker.conf 文件,内容如下。

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app.com/artisan queue:work sqs --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=root
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/app.com/worker.log
stopwaitsecs=3600

所有的目录都取决于你的服务器结构。配置文件创建完毕后,你需要用以下命令来激活监督器

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*

如果在实时服务器上配置队列工作者似乎太复杂了,有许多托管服务提供商有专门的工具,可以以更友好的方式完成这些功能,如Laravel ForgeDigitalocean

总结

Laravel队列是一个非常强大的工具,可以提高你的应用程序的性能,特别是当应用程序有许多沉重的,经常执行的任务。我们不希望让我们的用户等待,直到一个沉重的工作完成。因此,在一次点击之后,用户应该仍然可以自由地浏览其他页面或执行其他操作。然而,请记住,如果没有正确的配置或测试,后台进程会使服务器瘫痪。建议将大作业分成小作业,以避免进程执行超时。