Advertisement
  1. Code
  2. Laravel

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

Scroll to top
Read Time: 14 min

() translation by (you can also view the original English article)

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

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

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

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

Драйверы, соединения, очереди и задания

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

Драйверы очереди

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

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

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

Соединение

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

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

Очереди

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

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

Задания

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

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

Создайте свою первую работу в очереди

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

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

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

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

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

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

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

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

1
...
2
...
3
/*

4
|--------------------------------------------------------------------------

5
| Default Queue Driver

6
|--------------------------------------------------------------------------

7
|

8
| Laravel's queue API supports an assortment of back-ends via a single

9
| API, giving you convenient access to each back-end using the same

10
| syntax for each one. Here you may set the default queue driver.

11
|

12
| Supported: "sync", "database", "beanstalkd", "sqs", "redis", "null"

13
|

14
*/
15
16
'default' => env('QUEUE_DRIVER', 'database'),
17
...
18
...

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

1
$php artisan queue:table

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

1
<?php
2
use Illuminate\Support\Facades\Schema;
3
use Illuminate\Database\Schema\Blueprint;
4
use Illuminate\Database\Migrations\Migration;
5
6
class CreateJobsTable extends Migration
7
{
8
    /**

9
     * Run the migrations.

10
     *

11
     * @return void

12
     */
13
    public function up()
14
    {
15
        Schema::create('jobs', function (Blueprint $table) {
16
            $table->bigIncrements('id');
17
            $table->string('queue');
18
            $table->longText('payload');
19
            $table->unsignedTinyInteger('attempts');
20
            $table->unsignedInteger('reserved_at')->nullable();
21
            $table->unsignedInteger('available_at');
22
            $table->unsignedInteger('created_at');
23
24
            $table->index(['queue', 'reserved_at']);
25
        });
26
    }
27
28
    /**

29
     * Reverse the migrations.

30
     *

31
     * @return void

32
     */
33
    public function down()
34
    {
35
        Schema::dropIfExists('jobs');
36
    }
37
}

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

1
php artisan migrate

Это касается миграции jobs.

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

1
php artisan make:model Image --migration

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

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

1
<?php
2
// app/Image.php

3
namespace App;
4
5
use Illuminate\Database\Eloquent\Model;
6
7
class Image extends Model
8
{
9
    //

10
}

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

1
<?php
2
// database/migrations/YYYY_MM_DD_HHMMSS_create_images_table.php

3
use Illuminate\Support\Facades\Schema;
4
use Illuminate\Database\Schema\Blueprint;
5
use Illuminate\Database\Migrations\Migration;
6
7
class CreateImagesTable extends Migration
8
{
9
    /**

10
     * Run the migrations.

11
     *

12
     * @return void

13
     */
14
    public function up()
15
    {
16
        Schema::create('images', function (Blueprint $table) {
17
            $table->increments('id');
18
            $table->timestamps();
19
            $table->string('org_path');
20
        });
21
    }
22
23
    /**

24
     * Reverse the migrations.

25
     *

26
     * @return void

27
     */
28
    public function down()
29
    {
30
        Schema::dropIfExists('images');
31
    }
32
}

Как вы можете видеть, мы добавили столбец $table->string('org_path'), чтобы сохранить путь к исходному изображению. Затем вам просто нужно запустить команду migrate, чтобы фактически создать эту таблицу в базе данных.

1
$php artisan migrate

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

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

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

1
$php composer.phar require intervention/image

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

1
$php artisan make:job ProcessImageThumbnails

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

1
<?php
2
// app/Jobs/ProcessImageThumbnails.php

3
namespace App\Jobs;
4
5
use App\Image as ImageModel;
6
use Illuminate\Bus\Queueable;
7
use Illuminate\Queue\SerializesModels;
8
use Illuminate\Queue\InteractsWithQueue;
9
use Illuminate\Contracts\Queue\ShouldQueue;
10
use Illuminate\Foundation\Bus\Dispatchable;
11
use Illuminate\Support\Facades\DB;
12
13
class ProcessImageThumbnails implements ShouldQueue
14
{
15
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
16
17
    protected $image;
18
19
    /**

20
     * Create a new job instance.

21
     *

22
     * @return void

23
     */
24
    public function __construct(ImageModel $image)
25
    {
26
        $this->image = $image;
27
    }
28
29
    /**

30
     * Execute the job.

31
     *

32
     * @return void

33
     */
34
    public function handle()
35
    {
36
        // access the model in the queue for processing

37
        $image = $this->image;
38
        $full_image_path = public_path($image->org_path);
39
        $resized_image_path = public_path('thumbs' . DIRECTORY_SEPARATOR .  $image->org_path);
40
41
        // create image thumbs from the original image

42
        $img = \Image::make($full_image_path)->resize(300, 200);
43
        $img->save($resized_image_path);
44
    }
45
}

Когда работник очереди начинает обработку любого задания, он ищет метод handle. Таким образом, метод handle содержит основную логику вашей работы.

В нашем случае нам нужно создать миниатюру изображения, загруженного пользователем. Код метода handle довольно прост - мы извлекаем изображение из модели ImageModel и создаем эскиз, используя библиотеку Intervention Image. Конечно, нам нужно передать соответствующую модель Image, когда мы отправим наше задание, и мы увидим это через мгновение.

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

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

1
<?php
2
namespace App\Http\Controllers;
3
4
use App\Image;
5
use App\Jobs\ProcessImageThumbnails;
6
use Illuminate\Http\Request;
7
use Illuminate\Support\Facades\Redirect;
8
use App\Http\Controllers\Controller;
9
use Validator;
10
11
class ImageController extends Controller
12
{
13
    /**

14
     * Show Upload Form

15
     *

16
     * @param  Request  $request

17
     * @return Response

18
     */
19
    public function index(Request $request)
20
    {
21
        return view('upload_form');
22
    }
23
24
    /**

25
     * Upload Image

26
     *

27
     * @param  Request  $request

28
     * @return Response

29
     */
30
    public function upload(Request $request)
31
    {
32
        // upload image

33
        $this->validate($request, [
34
          'demo_image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
35
        ]);
36
        $image = $request->file('demo_image');
37
        $input['demo_image'] = time().'.'.$image->getClientOriginalExtension();
38
        $destinationPath = public_path('/images');
39
        $image->move($destinationPath, $input['demo_image']);
40
41
        // make db entry of that image

42
        $image = new Image;
43
        $image->org_path = 'images' . DIRECTORY_SEPARATOR . $input['demo_image'];
44
        $image->save();
45
46
        // defer the processing of the image thumbnails

47
        ProcessImageThumbnails::dispatch($image);
48
49
        return Redirect::to('image/index')->with('message', 'Image uploaded successfully!');
50
    }
51
}

Давайте создадим соответствующий файл отображения в resources/views/upload_form.blade.php.

1
<!DOCTYPE html>
2
<html lang="{{ config('app.locale') }}">
3
    <head>
4
        <meta charset="utf-8">
5
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
        <meta name="viewport" content="width=device-width, initial-scale=1">
7
        <meta name="csrf-token" content="{{ csrf_token() }}" />
8
        <title>Laravel</title>
9
10
        <!-- Fonts -->
11
        <link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">
12
13
        <!-- Styles -->
14
        <style>
15
            html, body {
16
                background-color: #fff;
17
                color: #636b6f;
18
                font-family: 'Raleway', sans-serif;
19
                font-weight: 100;
20
                height: 100vh;
21
                margin: 0;
22
            }
23
24
            .full-height {
25
                height: 100vh;
26
            }
27
28
            .flex-center {
29
                align-items: center;
30
                display: flex;
31
                justify-content: center;
32
            }
33
34
            .position-ref {
35
                position: relative;
36
            }
37
38
            .top-right {
39
                position: absolute;
40
                right: 10px;
41
                top: 18px;
42
            }
43
44
            .content {
45
                text-align: center;
46
            }
47
48
            .title {
49
                font-size: 84px;
50
            }
51
52
            .links > a {
53
                color: #636b6f;
54
                padding: 0 25px;
55
                font-size: 12px;
56
                font-weight: 600;
57
                letter-spacing: .1rem;
58
                text-decoration: none;
59
                text-transform: uppercase;
60
            }
61
62
            .m-b-md {
63
                margin-bottom: 30px;
64
            }
65
            
66
            .alert {
67
                color: red;
68
                font-weight: bold;
69
                margin: 10px;
70
            }
71
            .success {
72
                color: blue;
73
                font-weight: bold;
74
                margin: 10px;
75
            }
76
        </style>
77
    </head>
78
    <body>
79
        <div class="flex-center position-ref full-height">
80
            @if (Route::has('login'))
81
                <div class="top-right links">
82
                    @if (Auth::check())
83
                        <a href="{{ url('/home') }}">Home</a>
84
                    @else
85
                        <a href="{{ url('/login') }}">Login</a>
86
                        <a href="{{ url('/register') }}">Register</a>
87
                    @endif
88
                </div>
89
            @endif
90
91
            <div class="content">
92
                <div class="m-b-md">
93
                    <h1 class="title">Demo Upload Form</h1>
94
                    
95
                    @if ($errors->any())
96
                        <div class="alert alert-danger">
97
                            <ul>
98
                                @foreach ($errors->all() as $error)
99
                                    <li>{{ $error }}</li>
100
                                @endforeach
101
                            </ul>
102
                        </div>
103
                    @endif
104
                    
105
                    @if (session('message'))
106
                        <div class="success">
107
                            {{ session('message') }}
108
                        </div>
109
                    @endif
110
                    
111
                    <form method="post" action="{{ url('/image/upload') }}" enctype="multipart/form-data">
112
                      <div>
113
                        <input type="file" name="demo_image" />
114
                      </div>
115
                      <br/>
116
                      <div>
117
                        <input type="hidden" name="_token" value="{{ csrf_token() }}">
118
                        <input type="submit" value="Upload Image"/>
119
                      </div>
120
                    </form>
121
                </div>
122
            </div>
123
        </div>
124
    </body>
125
</html>

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

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

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

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

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

1
public function upload(Request $request)
2
{
3
    // upload image

4
    $this->validate($request, [
5
        'demo_image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
6
    ]);
7
    $image = $request->file('demo_image');
8
    $input['demo_image'] = time().'.'.$image->getClientOriginalExtension();
9
    $destinationPath = public_path('/images');
10
    $image->move($destinationPath, $input['demo_image']);
11
12
    // make db entry of that image

13
    $image = new Image;
14
    $image->org_path = 'images' . DIRECTORY_SEPARATOR . $input['demo_image'];
15
    $image->save();
16
17
    // defer the processing of the image thumbnails

18
    ProcessImageThumbnails::dispatch($image);
19
20
    return Redirect::to('image/index')->with('message', 'Image uploaded successfully!');
21
}

В начале метода upload вы увидите обычный код загрузки файлов, который перемещает загруженный файл в каталог public/images. Затем мы вставляем запись базы данных с использованием модели App/Image.

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

На данный момент задание добавляется в таблицу jobs для обработки. Подтвердите это, выполнив следующий запрос.

1
mysql> select * FROM lvl_jobs;
2
|  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, которая помогает нам запустить работника очереди.

1
$php artisan queue:work

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

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

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

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

Для этого уже есть несколько инструментов управления процессом, из которых вы можете выбрать. Вот этот список:

  • Circus
  • daemontools
  • Monit
  • Supervisor
  • Upstart

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

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

Заключение

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

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

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

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

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.