Статьи

Откладывание задач в Laravel с использованием очередей

В этой статье мы собираемся изучить API очереди в веб-фреймворке Laravel. Это позволяет вам откладывать ресурсоемкие задачи во время выполнения скрипта, чтобы улучшить общее восприятие конечного пользователя. После введения базовой терминологии я продемонстрирую ее на примере из реальной жизни.

Время загрузки страницы является важным аспектом любого успешного веб-сайта, и не следует упускать из виду важность этого, так как оно влияет на SEO сайта и общее восприятие конечного пользователя. Чаще всего вам приходится отлаживать веб-страницы с длительным временем загрузки. Конечно, есть разные подходы, которые можно использовать для устранения этой проблемы.

После исследования вы часто понимаете, что существуют определенные блоки кода, вызывающие задержку выполнения страницы. Следующее, что вы можете попробовать, — это определить блоки, которые можно отложить для обработки и которые не оказывают реального влияния на конечный результат текущей страницы. Это действительно должно улучшить общую скорость веб-страницы, поскольку мы устранили блоки кода, которые вызывали задержку.

Сегодня мы собираемся изучить аналогичную концепцию в контексте веб-фреймворка Laravel. Фактически, Laravel уже предоставляет полезный встроенный API, который позволяет нам откладывать обработку задач — Queue API. Не теряя много времени, я расскажу об основных элементах API очереди.

Основное назначение API очереди — запуск заданий, добавленных в очередь. Затем очередь может принадлежать определенному соединению, и это соединение может принадлежать определенному драйверу очереди, настроенному с этим соединением. Давайте кратко попробуем понять, что я только что сказал.

Точно так же, как вы бы использовали другой драйвер для подключения к базе данных, вы также можете выбрать один из множества различных драйверов очереди. API очереди поддерживает различные адаптеры, такие как база данных, beanstalkd, sqs и redis.

Драйвер очереди — это просто место, которое используется для хранения информации, связанной с очередью. Так, например, если вы используете драйвер очереди базы данных, новое задание будет добавлено в таблицу заданий в базе данных. С другой стороны, если вы настроили redis как драйвер очереди по умолчанию, задание будет добавлено на сервер redis.

API очереди также предоставляет два специальных драйвера очереди для тестирования — синхронизация и нулевое значение. Драйвер очереди синхронизации используется для немедленного выполнения задания очереди, в то время как драйвер нулевой очереди используется для пропуска задания, чтобы оно вообще не выполнялось.

Когда вы настраиваете API очереди в первый раз, вам нужно указать соединение по умолчанию, которое должно использоваться для обработки очереди по умолчанию. По крайней мере, ожидается, что соединение предоставит следующую информацию:

  • драйвер очереди, который будет использоваться
  • конкретные значения конфигурации драйвера очереди
  • имя очереди по умолчанию, в которое будет добавлено задание

Когда вы добавляете любое задание в очередь, оно будет добавлено в очередь по умолчанию. На самом деле, в большинстве случаев это будет хорошо, если только у вас нет рабочих мест, которым нужно отдавать приоритет перед другими. В этом случае вы можете создать очередь с именем high и поместить задания с более высоким приоритетом в эту конкретную очередь.

Когда вы запускаете работника очереди, который обрабатывает поставленные в очередь задания, вы можете при желании передать параметр --queue , который позволяет перечислять имена очередей в порядке их обработки. Например, если вы укажете --queue=high,default , он сначала будет обрабатывать задания в верхней очереди, а после завершения извлекает задания в очереди по умолчанию.

Задание в API очереди — это задание, которое отличается от основного потока выполнения. Например, если вы хотите создать миниатюру, когда пользователь загружает изображение из внешнего интерфейса, вы можете создать новое задание, которое обрабатывает обработку миниатюр. Таким образом, вы можете отложить задачу обработки миниатюр от основного потока выполнения.

Это было базовое введение в терминологию API очередей. В следующем разделе мы рассмотрим, как создать настраиваемое задание очереди и запустить его с помощью работника очереди Laravel.

К настоящему времени вы должны чувствовать уверенность в работе с очередями. Начиная с этого раздела, мы собираемся реализовать пример из реальной жизни, который демонстрирует концепцию заданий очереди в Laravel.

Чаще всего вы попадаете в ситуацию, когда вам нужно создавать разные уменьшенные версии изображения, загружаемого пользователем. В большинстве случаев разработчик пытается обработать его в режиме реального времени, чтобы разные версии изображений создавались сразу, когда пользователь загружает изображение.

Кажется, это разумный подход, если вы собираетесь создать пару версий, и это не займет много времени. С другой стороны, если вы имеете дело с приложением, которое требует интенсивной обработки и, следовательно, потребляет больше ресурсов, обработка в реальном времени может привести к ухудшению качества работы пользователя.

Очевидный вариант, который всплывает у вас на уме в первую очередь, — отложить обработку генерации миниатюр как можно позже. Самый простой подход, который вы могли бы реализовать в этом конкретном сценарии, — это установить задачу cron, которая запускает обработку через регулярные промежутки времени, и у вас все должно быть в порядке.

С другой стороны, гораздо лучший подход состоит в том, чтобы отложить и поместить задачу в очередь и позволить работнику очереди обработать ее, когда у нее появится такая возможность. В производственной среде работник очереди — это скрипт-демон, который всегда выполняется и обрабатывает задачи в очереди. Очевидным преимуществом этого подхода является гораздо лучшее взаимодействие с конечным пользователем, и вам не нужно ждать запуска cron, поскольку задание будет обработано как можно скорее.

Я предполагаю, что достаточно теории, чтобы начать с реальной реализации.

В нашем случае мы будем использовать драйвер очереди database , и для этого нам потребуется создать таблицу jobs в базе данных. Таблица jobs содержит все задания, которые должны быть обработаны при следующем запуске рабочего очереди.

Прежде чем мы продолжим создание таблицы jobs , давайте изменим конфигурацию очереди по умолчанию с sync на database в файле config/queue.php .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
/*
|—————————————————————————
|
|—————————————————————————
|
|
|
|
|
|
|
*/
 
‘default’ => env(‘QUEUE_DRIVER’, ‘database’),

Фактически, Laravel уже предоставляет команду ремесленников, которая помогает нам создавать таблицу jobs . Запустите следующую команду в корне вашего приложения Laravel, и оно должно создать необходимую миграцию базы данных, которая создает таблицу jobs .

1
$php artisan queue:table

Файл миграции, который создается в database/migrations/YYYY_MM_DD_HHMMSS_create_jobs_table.php должен выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateJobsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create(‘jobs’, function (Blueprint $table) {
            $table->bigIncrements(‘id’);
            $table->string(‘queue’);
            $table->longText(‘payload’);
            $table->unsignedTinyInteger(‘attempts’);
            $table->unsignedInteger(‘reserved_at’)->nullable();
            $table->unsignedInteger(‘available_at’);
            $table->unsignedInteger(‘created_at’);
 
            $table->index([‘queue’, ‘reserved_at’]);
        });
    }
 
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists(‘jobs’);
    }
}

Далее, давайте запустим команду migrate чтобы она фактически jobs таблицу jobs в базе данных.

1
php artisan migrate

Вот и все, что касается миграции jobs .

Далее, давайте создадим модель Image которая будет использоваться для управления изображениями, загруженными конечным пользователем. Для модели изображения также требуется связанная таблица базы данных, поэтому мы будем использовать опцию --migrate при создании модели Image .

1
php artisan make:model Image —migration

Приведенная выше команда должна также создать класс модели Image и связанную миграцию базы данных.

Класс модели Image должен выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
<?php
// app/Image.php
namespace App;
 
use Illuminate\Database\Eloquent\Model;
 
class Image extends Model
{
    //
}

И файл миграции базы данных должен быть создан в database/migrations/YYYY_MM_DD_HHMMSS_create_images_table.php . Мы также хотим сохранить исходный путь к изображению, загруженному конечным пользователем. Давайте пересмотрим код файла миграции базы данных Image чтобы он выглядел следующим образом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?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 чтобы фактически создать эту таблицу в базе данных.

1
$php artisan migrate

И это все, что касается модели Image .

Далее, давайте создадим реальное задание очереди, которое отвечает за обработку миниатюр изображений. Для обработки миниатюр мы будем использовать очень популярную библиотеку обработки изображений — Intervention Image.

Чтобы установить библиотеку Intervention Image, выполните следующую команду в корневом каталоге вашего приложения.

1
$php composer.phar require intervention/image

Теперь пришло время создать класс Job , и мы будем использовать команду для этого.

1
$php artisan make:job ProcessImageThumbnails

Это должно создать шаблон класса Job в app/Jobs/ProcessImageThumbnails.php . Давайте заменим содержимое этого файла следующим.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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 и создаем миниатюру с помощью библиотеки изображений Intervention. Конечно, нам нужно передать соответствующую модель Image когда мы отправим свою работу, и мы увидим это через мгновение.

Чтобы протестировать нашу вновь созданную работу, мы создадим простую форму загрузки, которая позволяет пользователю загружать изображение. Конечно, мы не будем сразу создавать эскизы изображений; мы отложим эту задачу, чтобы ее мог обработать работник очереди.

Давайте создадим файл контроллера в app/Http/Controllers/ImageController.php как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?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’ .
        $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 .

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<!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;
            }
 
            .mb-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 class=»content»>
                <div class=»mb-md»>
                    <h1 class=»title»>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 class=»success»>
                            {{ 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>

Наконец, давайте добавим маршруты для index и routes/web.php действия в файле routes/web.php .

1
2
Route::get(‘image/index’, ‘ImageController@index’);
Route::post(‘image/upload’, ‘ImageController@upload’);

В контроллере ImageController метод index используется для отображения формы загрузки.

1
2
3
4
public function index(Request $request)
{
    return view(‘upload_form’);
}

Когда пользователь отправляет форму, вызывается метод upload .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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’ .
    $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 для обработки. Давайте подтвердим это, выполнив следующий запрос.

1
2
mysql> select * FROM lvl_jobs;
|

Вы, должно быть, задаетесь вопросом, что нужно для обработки работы тогда? Не волнуйтесь — это то, что мы собираемся обсудить в следующем разделе.

Задача работника очереди Laravel состоит в обработке заданий, поставленных в очередь для обработки. Фактически, есть команда ремесленника, которая помогает нам запустить рабочий процесс очереди.

1
$php artisan queue:work

Как только вы запускаете эту команду, она обрабатывает отложенные задания. В нашем случае он должен обработать задание ProcessImageThumbnails которое было поставлено в очередь, когда пользователь ранее загрузил изображение.

1
2
3
$php artisan queue:work
[YYYY-MM-DD HHMMSS] Processing: App\Jobs\ProcessImageThumbnails
[YYYY-MM-DD HHMMSS] Processed: App\Jobs\ProcessImageThumbnails

Вы могли заметить, что когда вы запускаете работника очереди, он продолжает работать, пока вы не убьете его вручную или не закроете терминал. На самом деле, он ждет следующей работы для обработки. Как только в очереди появится новое задание, оно будет сразу же обработано, если работает работник очереди.

Конечно, мы не можем продолжать работать таким образом, поэтому нам нужно найти способ для работника очереди постоянно работать в фоновом режиме.

К нашему спасению, есть несколько инструментов управления процессами, которые вы можете выбрать. Чтобы назвать несколько, вот список:

  • Цирк
  • DAEMON Tools
  • монит
  • Руководитель
  • Выскочка

Вы должны выбрать инструмент, который вам удобен для управления работником очереди Laravel. По сути, мы хотим убедиться, что работник очереди должен работать неопределенно долго, чтобы он сразу обрабатывал поставленные в очередь задания.

Так что это API очереди в вашем распоряжении. Вы можете использовать его в своей повседневной разработке, чтобы отложить трудоемкие задачи для улучшения работы конечного пользователя.

В этой статье мы обсудили API очереди в Laravel, который действительно полезен, если вы хотите отложить обработку ресурсоемких задач.

Мы начали с базового введения в API очереди, которое включало обсуждение соединений, очередей и заданий. Во второй половине статьи мы создали специальное задание очереди, которое продемонстрировало, как вы можете использовать API очереди в реальном мире.

Для тех из вас, кто только начинает работать с Laravel или хочет расширить свои знания, сайт или приложение с помощью расширений, у нас есть множество вещей, которые вы можете изучить на Envato Market .

Не стесняйтесь использовать форму обратной связи ниже, чтобы оставить свои вопросы и предложения.