---
title: "Дебаунсинг черг подій у Laravel: як уникнути марних виконань слухачів"
url: https://laravelukraine.com/blog/debaunsing-cerg-podii-u-laravel-iak-uniknuti-marnix-vikonan-sluxaciv
date: 2026-08-22
source: https://laravel-news.com/laravel-debounced-queued-listeners?utm_medium=feed&utm_source=feedpress.me&utm_campaign=Feed%3A+laravelnews
---

# Дебаунсинг черг подій у Laravel: як уникнути марних виконань слухачів

Уявіть ситуацію: імпорт продуктів торкається одного й того ж запису сорок разів за хвилину. Подія `ProductUpdated` спрацьовує сорок разів, а слухач, який перебудовує пошуковий індекс, виконується сорок разів. Кожен запуск індексує стан, який наступний одразу перезаписує. Черга робить саме те, що їй сказали, але 97% роботи - марна трата ресурсів.

Що насправді потрібно: щоб серія однакових подій схлопнулася в один запуск слухача наприкінці серії, передавши лише останній актуальний стан.

## Дебаунсинг для слухачів подій

Laravel 13.6 додав таке схлопування для черг завдань у вигляді debounceable queued jobs. Тепер Laravel 13.26 розширює той самий атрибут `#[DebounceFor]` на слухачі подій у черзі. Цей внесок зробив [@stevebauman](https://github.com/stevebauman) у [
#61169](https://github.com/laravel/framework/pull/61169), тож тепер event-driven код отримує цю поведінку без необхідності реструктуризації слухачів у завдання, що диспатчаться вручну.

Раніше цю прогалину заповнювали пакети спільноти, зокрема Laravel Debounce. Власна відповідь фреймворку почалася з завдань, а тепер поширилася й на слухачі.

## Як додати дебаунсинг до слухача

Достатньо додати атрибут до слухача в черзі й вказати вікно дебаунсингу в секундах:

```php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30, maxWait: 120)]
class UpdateProductSearchIndex implements ShouldQueue
{
    public function debounceId(ProductUpdated $event): string
    {
        return (string) $event->product->getKey();
    }

    public function handle(ProductUpdated $event): void
    {
        ProductIndexer::index($event->product->fresh());
    }
}
```

Тепер кожна подія `ProductUpdated` для продукту з ID 42 у межах 30-секундного вікна призведе до одного виклику `handle()`, який виконається для останньої події серії. Події для продукту 43 дебаунсяться незалежно, оскільки метод `debounceId()` створює окреме вікно для кожного продукту.

Якщо `debounceId()` не визначений, усі диспатчі слухача ділять одне вікно - саме така поведінка потрібна для слухачів на кшталт "перебудувати карту сайту", які не залежать від конкретного ресурсу. ID також може бути звичайною властивістю `$debounceId`, коли він не залежить від події.

## Принцип роботи дебаунсингу

При дебаунсингу перемагає останній диспатч. Кожен диспатч ставить слухача в чергу із затримкою, що дорівнює вікну дебаунсингу, і записує токен власника в кеш з ключем, що складається з класу слухача та ID дебаунсингу. Новіший диспатч перезаписує токен, і коли стара копія з черги нарешті виконується, вона бачить, що більше не володіє токеном, і відкидає себе.

Один нюанс випливає з дизайну на основі затримки: тихий ресурс все одно чекає повне вікно. Тому одна подія для неактивного продукту виконається через 30 секунд, а не негайно.

## maxWait і проблема голодування

Чистий дебаунсинг має режим відмови: потік подій, який ніколи не зупиняється на 30 секунд, відкладає слухача назавжди. Саме для цього існує параметр `maxWait`.

Із `#[DebounceFor(30, maxWait: 120)]` коли диспатчі просувають вікно протягом 120 секунд, наступний диспатч виконається без затримки замість чергового продовження відкладення. Активний імпорт все одно отримає схлопнення записів - приблизно один запуск індексації на дві хвилини замість сорока запусків або нуля.

## Додаткові налаштування

Два регулятори взаємодіють із затримкою:

- Явна затримка, встановлена на слухачі, має пріоритет над тією, що походить від дебаунсингу
- Слухач може перевизначити сховище кешу, яке керує токенами власності, визначивши метод `debounceVia()` - корисно, коли ваш типовий кеш не є спільним для кожного сервера, що диспатчить події

## Три важливі обмеження

Перед впровадженням варто знати три обмеження:

### 1. Несумісність із ShouldBeUnique

Не можна поєднувати з `ShouldBeUnique`. Ці дві функції тримають протилежні блокування: "перемагає перший" проти "перемагає останній". Їх комбінування тепер викидає `LogicException` при диспатчі замість мовчазного вибору переможця.

### 2. Область дії - слухач, а не подія

Інші слухачі на `ProductUpdated` виконуються для кожної події. Тільки слухач із атрибутом схлопується. Дебаунсинг - це властивість роботи, а не потоку подій.

### 3. Обробник бачить тригерну подію, тому перечитуйте стан

Об'єкт події, що переживає дебаунсинг, - це останній диспатчений, але до моменту виконання навіть він може застаріти. Саме тому приклад вище викликає `$event->product->fresh()`. Дебаунсований слухач має трактувати подію як вказівник на ресурс, а не як корисне навантаження.

Саме ця остання звичка робить дебаунсинг безпечним: якщо слухач перевиводить свій результат із бази даних, схлопування сорока запусків в один змінює вартість, а не результат.
