在本文中,我们将探索 Laravel Web 框架中的 Queue api。它允许您在脚本执行期间推迟资源密集型任务,以增强整体最终用户体验。在介绍了基本术语之后,我将通过实现一个真实的示例来演示它。
页面加载时间是任何成功网站的重要方面,因为它会影响网站的 SEO 和整体用户体验。通常情况下,您最终希望调试页面加载时间较长的网页。当然,您可以使用不同的方法来纠正此问题。
经过调查,您经常意识到某些代码块会导致页面执行延迟。您可以尝试的下一件事是识别可以延迟处理并且对当前页面的最终结果没有实际影响的块。这应该会提高整体网页速度,因为我们已经消除了导致延迟的代码块。
今天,我们将在 Laravel Web 框架的上下文中探索一个类似的概念。事实上,Laravel 已经提供了一个有用的内置 API,它允许我们延迟任务的处理——队列 API。在不浪费您太多时间的情况下,我将继续讨论 Queue API 的基本元素。
驱动程序、连接、队列和作业
Queue API 的基本目的是运行添加到队列中的作业。接下来,队列可能属于特定连接,并且该连接可能属于使用该连接本身配置的特定队列驱动程序。让我们简要地尝试理解我刚才所说的话。
队列驱动程序
就像您为数据库连接使用不同的驱动程序一样,您也可以从各种不同的队列驱动程序中进行选择。Queue API 支持不同的适配器,如数据库、beantalkd、sqs 和 redis。
队列驱动只是一个用来存储队列相关信息的地方。因此,例如,如果您使用数据库队列驱动程序,新作业将被添加jobs
到数据库的表中。另一方面,如果您已配置redis
为默认队列驱动程序,则作业将添加到 redis 服务器。
队列 API 还提供了两个特殊的队列驱动程序用于测试目的sync
- 和null
. sync
队列驱动程序用于立即执行队列作业,而队列null
驱动程序用于跳过作业,使其根本不会执行。
连接
首次配置 Queue API 时,您需要指定一个默认连接,用于默认队列处理。至少,该连接应提供以下信息:
将使用的队列驱动程序
队列驱动程序的特定配置值
将在其中添加作业的默认队列名称
尾巴
当您将任何作业添加到队列中时,它将被添加到default
队列中。在大多数情况下,这应该没问题,除非您的工作需要比其他工作具有更高的优先级。在这种情况下,您可以创建一个名为high的队列并将较高优先级的作业放置在该特定队列中。
当您运行处理排队作业的队列工作程序时,您可以选择传递--queue
参数,该参数允许您按需要处理的顺序列出队列名称。例如,如果您指定--queue=high,default
,它将首先处理高队列中的作业,一旦完成,它将在默认队列中获取es 作业。
工作
队列 API 中的作业是从主执行流程延迟的任务。例如,如果您想在用户从前端上传图像时创建缩略图,您可以创建一个处理缩略图处理的新作业。通过这种方式,您可以将缩略图处理任务从主执行流程中推迟。
这是对队列 API 术语的基本介绍。从下一节开始,我们将探索如何创建自定义队列作业并使用 Laravel 队列工作程序运行它。
快速查看队列配置文件
在前面的部分中,我们已经了解了 Laravel 中的队列术语。让我们快速浏览一下config/queue.php配置文件,以便您可以将它与我们迄今为止讨论的概念联系起来。
<?php return [ /* |-------------------------------------------------------------------------- | Default Queue Connection Name |-------------------------------------------------------------------------- | | Laravel's queue API supports an assortment of back-ends via a single | API, giving you convenient access to each back-end using the same | syntax for every one. Here you may define a default connection. | */ 'default' => env('QUEUE_CONNECTION', 'sync'), /* |-------------------------------------------------------------------------- | Queue Connections |-------------------------------------------------------------------------- | | Here you may configure the connection information for each server that | is used by your application. A default configuration has been added | for each back-end shipped with Laravel. You are free to add more. | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" | */ 'connections' => [ 'sync' => [ 'driver' => 'sync', ], 'database' => [ 'driver' => 'database', 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, ], 'beanstalkd' => [ 'driver' => 'beanstalkd', 'host' => 'localhost', 'queue' => 'default', 'retry_after' => 90, 'block_for' => 0, ], 'sqs' => [ 'driver' => 'sqs', 'key' => env('aws_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 'queue' => env('SQS_QUEUE', 'your-queue-name'), 'suffix' => env('SQS_SUFFIX'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], 'redis' => [ 'driver' => 'redis', 'connection' => 'default', 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => 90, 'block_for' => null, ], ], /* |-------------------------------------------------------------------------- | Failed Queue Jobs |-------------------------------------------------------------------------- | | These options configure the behavior of failed queue job logging so you | can control which database and table are used to store the jobs that | have failed. You may change them to any database / table you wish. | */ 'failed' => [ 'driver' => env('QUEUE_FAILED_DRIVER', 'database'), 'database' => env('DB_CONNECTION', 'mysql'), 'table' => 'failed_jobs', ], ];
如您所见,您可以从几组不同的连接中进行选择。对于每个连接,您可以配置特定于连接的信息、队列驱动程序和默认队列名称。更重要的是,您还可以在键下设置要使用的默认队列连接default
。
这就是队列配置文件的基础。
创建您的第一个队列作业
到目前为止,您应该对队列作业充满信心。从本节开始,我们将实现一个真实世界的示例来演示 Laravel 中队列作业的概念。
您通常需要为用户上传的图像创建不同的缩略图版本。在大多数情况下,开发人员会尝试实时处理它,以便在用户上传图像时立即创建不同版本的图像。
如果您要创建几个版本,这似乎是一种合理的方法,而且一开始不会花费太多时间。另一方面,如果您正在处理需要大量处理并因此占用更多资源的应用程序,则实时处理最终可能会导致糟糕的用户体验。
首先出现在您脑海中的明显选项是尽可能晚地推迟缩略图生成的处理。您可以在此特定场景中实现的最简单方法是设置一个定期触发处理的 cron 作业,您应该没问题。
另一方面,更好的方法是将任务推迟并推送到队列中,并让队列工作人员在有机会时处理它。在生产环境中,队列工作者是一个守护程序脚本,它始终在队列中运行和处理任务。这种方法的明显好处是更好的最终用户体验,并且您不必等待 cron 运行,因为作业会尽快得到处理。
我想这已经足够理论来开始实际的实现了。
创建jobs
表
在我们的例子中,我们将使用database
队列驱动程序,它需要我们jobs
在数据库中创建表。该jobs
表包含需要在下一次队列工作者运行中处理的所有作业。
在我们继续创建表之前,让我们将文件中jobs
的默认队列配置从 更改sync
为。database
.env
... ... QUEUE_CONNECTION=database ... ...
事实上,Laravel 已经提供了一个 artisan 命令来帮助我们创建jobs
表。在 Laravel 应用程序的根目录中运行以下命令,它应该创建创建jobs
表所需的数据库迁移。
$php artisan queue:table
在database/migrations/YYYY_MM_DD_HHMMSS_create_jobs_table.php生成的迁移文件应如下所示:
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateJobsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('jobs', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('queue')->index(); $table->longText('payload'); $table->unsignedTinyInteger('attempts'); $table->unsignedInteger('reserved_at')->nullable(); $table->unsignedInteger('available_at'); $table->unsignedInteger('created_at'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('jobs'); } }
接下来,让我们运行migrate
命令,以便它实际jobs
在数据库中创建表。
$php artisan migrate
就jobs
迁移而言,就是这样。
创建Image
模型
接下来,让我们创建Image
将用于管理最终用户上传的图像的模型。图像模型还需要一个关联的数据库表,因此我们将在创建模型时使用该--migrate
选项。Image
$php artisan make:model Image --migration
上面的命令也应该创建Image
模型类和关联的数据库迁移。
Image
模型类应如下所示:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Image extends Model { // }
并且数据库迁移文件应该在database/migrations/YYYY_MM_DD_HHMMSS_create_images_table.php创建。我们还想存储最终用户上传的图片的原始路径。让我们修改Image
数据库迁移文件的代码,如下所示。
<?php // database/migrations/YYYY_MM_DD_HHMMSS_create_images_table.php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateImagesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('images', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); $table->string('org_path'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('images'); } }
如您所见,我们添加了$table->string('org_path')
用于存储原始图像路径的列。接下来,您只需要运行migrate
命令即可在数据库中实际创建该表。
php artisan migrate
就Image
模型而言,就是这样。
创建一个 Laravel 作业
接下来,让我们创建一个负责处理图像缩略图的实际队列作业。对于缩略图处理,我们将使用一个非常流行的图像处理库——Intervention Image。
要安装 Intervention Image 库,请继续并在应用程序的根目录下运行以下命令。
php composer.phar require intervention/image
现在,是时候创建Job
类了,我们将使用 artisan 命令来完成。
php artisan make:job ProcessImageThumbnails
这应该在app/Jobs/ProcessImageThumbnails.phpJob
创建类模板。让我们用以下内容替换该文件的内容。
<?php // app/Jobs/ProcessImageThumbnails.php namespace App\Jobs; use App\Image as ImageModel; use Illuminate\Bus\Queueable; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Support\Facades\DB; class ProcessImageThumbnails implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $image; /** * Create a new job instance. * * @return void */ public function __construct(ImageModel $image) { $this->image = $image; } /** * Execute the job. * * @return void */ public function handle() { // access the model in the queue for processing $image = $this->image; $full_image_path = public_path($image->org_path); $resized_image_path = public_path('thumbs' . DIRECTORY_SEPARATOR . $image->org_path); // create image thumbs from the original image $img = \Image::make($full_image_path)->resize(300, 200); $img->save($resized_image_path); } }
当队列工作者开始处理任何作业时,它会查找该handle
方法。所以它handle
是保持你工作的主要逻辑的方法。
在我们的例子中,我们需要创建用户上传的图像的缩略图。该handle
方法的代码非常简单——我们从ImageModel
模型中检索图像并使用干预图像库创建缩略图。当然,我们Image
在调度我们的工作时需要传递相应的模型,我们稍后会看到。
如何测试工作
为了测试我们新创建的作业,我们将创建一个允许用户上传图像的简单上传表单。当然,我们不会立即创建图像缩略图;我们将推迟该任务,以便它可以由队列工作人员处理。
让我们在app/Http/Controllers/ImageController.php创建一个控制器文件,如下所示。
<?php namespace App\Http\Controllers; use App\Image; use App\Jobs\ProcessImageThumbnails; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; use App\Http\Controllers\Controller; use Validator; class ImageController extends Controller { /** * Show Upload Form * * @param Request $request * @return Response */ public function index(Request $request) { return view('upload_form'); } /** * Upload Image * * @param Request $request * @return Response */ public function upload(Request $request) { // upload image $this->validate($request, [ 'demo_image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048', ]); $image = $request->file('demo_image'); $input['demo_image'] = time().'.'.$image->getClientOriginalExtension(); $destinationPath = public_path('/images'); $image->move($destinationPath, $input['demo_image']); // make db entry of that image $image = new Image; $image->org_path = 'images' . DIRECTORY_SEPARATOR . $input['demo_image']; $image->save(); // defer the processing of the image thumbnails ProcessImageThumbnails::dispatch($image); return Redirect::to('image/index')->with('message', 'Image uploaded successfully!'); } }
让我们在resources/views/upload_form.blade.php创建一个关联的视图文件。
<!DOCTYPE html> <html lang="{{ config('app.locale') }}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" content="{{ csrf_token() }}" /> <title>Laravel</title> <!-- fonts --> <link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css"> <!-- Styles --> <style> html, body { background-color: #fff; color: #636b6f; font-family: 'Raleway', sans-serif; font-weight: 100; height: 100vh; margin: 0; } .full-height { height: 100vh; } .flex-center { align-items: center; display: flex; justify-content: center; } .position-ref { position: relative; } .top-right { position: absolute; right: 10px; top: 18px; } .content { text-align: center; } .title { font-size: 84px; } .links > a { color: #636b6f; padding: 0 25px; font-size: 12px; font-weight: 600; letter-spacing: .1rem; text-decoration: none; text-transform: uppercase; } .m-b-md { margin-bottom: 30px; } .alert { color: red; font-weight: bold; margin: 10px; } .success { color: blue; font-weight: bold; margin: 10px; } </style> </head> <body> <div class="flex-center position-ref full-height"> @if (Route::has('login')) <div class="top-right links"> @if (Auth::check()) <a href="{{ url('/home') }}">Home</a> @else <a href="{{ url('/login') }}">Login</a> <a href="{{ url('/register') }}">Register</a> @endif </div> @endif <div> <div> <h1>Demo Upload Form</h1> @if ($errors->any()) <div class="alert alert-danger"> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif @if (session('message')) <div> {{ session('message') }} </div> @endif <form method="post" action="{{ url('/image/upload') }}" enctype="multipart/form-data"> <div> <input type="file" name="demo_image" /> </div> <br/> <div> <input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="submit" value="Upload Image"/> </div> </form> </div> </div> </div> </body> </html>
最后,让我们 在routes/web.php文件中为index
和upload
动作添加路由。
Route::get('image/index', 'ImageController@index'); Route::post('image/upload', 'ImageController@upload');
在ImageController
控制器中,该index
方法用于呈现上传表单。
public function index(Request $request) { return view('upload_form'); }
当用户提交表单时,将upload
调用该方法。
public function upload(Request $request) { // upload image $this->validate($request, [ 'demo_image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048', ]); $image = $request->file('demo_image'); $input['demo_image'] = time().'.'.$image->getClientOriginalExtension(); $destinationPath = public_path('/images'); $image->move($destinationPath, $input['demo_image']); // make db entry of that image $image = new Image; $image->org_path = 'images' . DIRECTORY_SEPARATOR . $input['demo_image']; $image->save(); // defer the processing of the image thumbnails ProcessImageThumbnails::dispatch($image); return Redirect::to('image/index')->with('message', 'Image uploaded successfully!'); }
在该upload
方法的开头,您会注意到将上传的文件移动到public/images
目录的常用文件上传代码。接下来,我们使用模型插入数据库记录App/Image
。
最后,我们使用ProcessImageThumbnails
作业来推迟缩略图处理任务。重要的是要注意它是dispatch
用于推迟任务的方法。最后,用户将被重定向到上传页面并显示成功消息。
此时,作业被添加到jobs
表中进行处理。让我们通过发出以下查询来确认它。
mysql> select * FROM jobs; | 1 | default | {"displayName":"App\\Jobs\\ProcessImageThumbnails","job":"Illuminate\\Queue\\CallQueuedHandler@call","maxTries":null,"timeout":null,"data":{"commandName":"App\\Jobs\\ProcessImageThumbnails","command":"O:31:\"App\\Jobs\\ProcessImageThumbnails\":5:{s:8:\"\u0000*\u0000image\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":2:{s:5:\"class\";s:9:\"App\\Image\";s:2:\"id\";i:2;}s:6:\"\u0000*\u0000job\";N;s:10:\"connection\";N;s:5:\"queue\";N;s:5:\"delay\";N;}"}} | 0 | NULL | 1510219099 | 1510219099 |
您一定想知道,那么处理一份工作需要什么?别担心——这就是我们将在下一节中讨论的内容。
队列工作者
Laravel 队列工作者的工作是处理排队等待处理的作业。事实上,有一个 artisan 命令可以帮助我们启动队列工作进程。
$php artisan queue:work
一旦您运行该命令,它就会处理待处理的作业。在我们的例子中,它应该处理ProcessImageThumbnails
用户之前上传图片时排队的作业。
$php artisan queue:work [YYYY-MM-DD HHMMSS] Processing: App\Jobs\ProcessImageThumbnails [YYYY-MM-DD HHMMSS] Processed: App\Jobs\ProcessImageThumbnails
您会注意到,当您启动队列工作者时,它会一直运行,直到您手动终止它或关闭终端。实际上,它正在等待处理下一个作业。一旦队列中有新作业,如果队列工作者正在运行,它将立即处理。
当然,我们不能让它一直这样运行,所以我们需要想办法让队列工作者在后台永久运行。
为了我们的救援,您可以选择多种流程管理工具。仅举几例,这里有一个列表:
circus
daemon
quickly
tutor
upstart
你应该选择一个你熟悉的工具来管理 Laravel 队列工作者。基本上,我们要确保队列工作者应该无限期地运行,以便它立即处理排队的作业。
这就是您可以使用的 Queue API。您可以在日常开发中使用它来推迟耗时的任务,从而改善最终用户体验。
结论
在本文中,我们讨论了 Laravel 中的 Queue API,如果您希望延迟处理资源消耗任务,这真的很有帮助。
我们从对 Queue API 的基本介绍开始,其中涉及对连接、队列和作业的讨论。在文章的后半部分,我们创建了一个自定义队列作业,演示了如何在现实世界中使用队列 API。
- 队列驱动程序
- 连接
- 尾巴
- 工作
- 快速查看队列配置文件
- 创建jobs表
- 创建Image模型
- 创建一个 Laravel 作业
- 如何测试工作