Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама
Reservable-моделі в Laravel: як уникнути дублювання обробки за допомогою атомарних блокувань
Новини 05 жовтня 2026

Reservable-моделі в Laravel: як уникнути дублювання обробки за допомогою атомарних блокувань

Під час перебудови свого персонального сайту Аарон Френсіс працює з багатьма сторонніми сервісами: завантажує відео з YouTube, транскрибує аудіо, просить OpenAI зробити підсумки транскриптів тощо. У статті він ділиться підходом, який назвав «резервованими моделями».

Вихідна ситуація: scopes та команди

Автор активно використовує Eloquent scopes, щоб визначити, що саме потрібно зробити. Ось кілька scopes із моделі Video:

public function scopeReadyForDownload(Builder $query): void
{
    $query->whereNull('video_path');
}

public function scopeReadyForAudio(Builder $query): void
{
    $query->whereNotNull('video_path')->whereNull('audio_path');
}

public function scopeReadyForTranscript(Builder $query): void
{
    $query->whereNotNull('audio_path')->whereDoesntHave('transcripts');
}

public function scopeReadyForThumbnails(Builder $query): void
{
    $query->whereNotNull('video_path')->whereNull('thumbnails');
}

Для кожного завдання існує окрема команда, яка бере моделі й виконує роботу. Ось команда Download:

class Download extends Command
{
    protected $signature = 'youtube:download';

    protected $description = 'Download YouTube videos and put them on S3.';

    public function handle(): void
    {
        Video::readyForDownload()->each(function (Video $video) {
            Log::info('Downloading ' . $video->title);
            // Do something interesting
        });
    }
}

Дві проблеми нерезервованого підходу

Перша проблема: залежно від тривалості виконання команди, частоти її запуску та інших реальних чинників модель може пройти через воркфлоу більше одного разу. Можна додати withoutOverlapping у розкладі (автор це рекомендує), але хтось усе одно може запустити команду вручну, і обробки перетнуться.

Друга проблема стосується не дублювання, а «стовпотворіння» при збоях. Якщо команда запускається щохвилини і одне конкретне відео постійно падає, можна спробувати завантажити його з YouTube 60 разів на годину, що навряд чи сподобається YouTube. Потрібен своєрідний бар'єр, який не дасть постійно навантажувати зовнішні ресурси при повторних помилках.

Резервування моделей через кеш-блокування

Замість домовленостей у команді автор пропонує вирішити проблему раз і назавжди через cache locks. У Laravel є надійний механізм атомарних блокувань, який працює з різними драйверами, зокрема з драйвером бази даних. Про атомарні блокування в Laravel також є відео автора: Laravel solved race conditions.

Атомарні блокування гарантують, що лише один процес утримує lock. Але замість прямої роботи з блокуванням автор звертається до нього через модель, адже мета - резервувати саме моделі. Для цього створюється трейт Reservable з двома методами:

trait Reservable
{
    public function reserve(mixed $key, string|int|Carbon $duration = 60): bool
    {
        //
    }

    public function release(mixed $key): void
    {
        //
    }
}

Використання виглядає так:

$reserved = $video->reserve('download', '+1 hour');

Розробнику не потрібно думати про деталі кеш-системи: він просто намагається зарезервувати модель для певної цілі та отримує відповідь «так» або «ні».

Метод reservation

Основна логіка живе в методі reservation, який повертає екземпляр Illuminate\Contracts\Cache\Lock:

public function reservation(mixed $key, string|int|Carbon $duration = 60): Lock
{
    // Convert e.g. +6 hours to a Carbon instance
    if (is_string($duration)) {
        $duration = Carbon::make($duration);
    }

    // Convert Carbon to seconds from now
    if ($duration instanceof Carbon) {
        $duration = max(0, $duration->diffInSeconds(now()));
    }

    // Convert enums to strings
    if ($key instanceof UnitEnum) {
        $key = $key->name;
    }

    // Convert objects to strings
    if (is_object($key)) {
        $key = get_class($key);
    }

    // Use the most stable methods of representing a model.
    return Cache::lock(
        "{$this->getMorphClass()}:{$this->getKey()}:{$key}", $duration
    );
}

Інтерфейс вийшов гнучким: як $key можна передати майже будь-що, а як $duration - рядок, число секунд або Carbon. Автор пояснює, навіщо це, далі в статті.

Завершена реалізація

public function reserve(mixed $key, string|int|Carbon $duration = 60): bool
{
    return $this->reservation($key, $duration)->get();
}

public function release(mixed $key): void
{
    $this->reservation($key)->forceRelease();
}

У reserve створюється lock і виконується спроба його get, що повертає булеве значення успіху чи невдачі. У release lock створюється і звільняється через forceRelease. Тут потрібен саме forceRelease, бо поточний код не є власником блокування: $owner не передавався, тож він має випадкове значення. Зазвичай чужі блокування звільняти не варто, тому цей метод слід використовувати, лише чітко розуміючи навіщо. У цьому випадку це виправдано, бо блокування дуже вузько обмежені за призначенням.

Використання трейта Reservable

Тепер трейт легко додати до команди Download:

class Download extends Command
{
    protected $signature = 'youtube:download';

    protected $description = 'Download YouTube videos and put them on S3.';

    public function handle(): void
    {
        $video = Video::query()
            ->readyForDownload()
            ->get()
            // Get the first one that can be reserved for this command, for 6 hours.
            ->first(fn(Video $video) => $video->reserve($this, '+6 hours'));

        if (!$video) {
            return;
        }

        Log::info('Downloading ' . $video->title);
        // Do something interesting
    }
}

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

video:249:App\Console\Commands\YouTube\Download

Звільняти блокування не потрібно - його можна просто залишити до завершення терміну дії. Якщо команда виконалась успішно, блокування вже неважливе: відео не треба завантажувати знову, а scope виключить його з вибірки. Якщо ж команда впала, модель має лишатися заблокованою на 6 годин, щоб запобігти безперервним помилкам.

Макроси

Рядок із first(...) можна зробити охайнішим за допомогою макросу для Eloquent Collection:

Collection::macro('firstReserved', function (mixed $key, string|int|Carbon $duration = 60) {
    return $this->first(fn($item) => $item->reserve($key, $duration));
});

Тоді команда виглядає так:

class Download extends Command
{
    public function handle(): void
    {
        $video = Video::readyForDownload()->get()->firstReserved($this, '+6 hours');

        if (!$video) {
            return;
        }

        Log::info('Downloading ' . $video->title);
        // Do something interesting
    }
}

Макрос можна додати навіть до Builder:

Builder::macro('firstReserved', function (mixed $key, string|int|Carbon $duration = 60) {
    return $this->get()->first(fn($item) => $item->reserve($key, $duration));
});

Тоді зайвий виклик get посередині не потрібен:

class Download extends Command
{
    public function handle(): void
    {
        $video = Video::readyForDownload()->firstReserved($this, '+6 hours');

        if (!$video) {
            return;
        }

        Log::info('Downloading ' . $video->title);
        // Do something interesting
    }
}

Автор нещодавно почав користувати цей підхід і вважає його зручним, але запрошує читачів ділитися ідеями щодо покращень - зворотний зв'язок можна залишити у його акаунті в Twitter.

9

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

PHPStan 2.3: стрибок продуктивності, покращені generics і виявлення невикористаних змінних
Новини 06 жовтня 2026

PHPStan 2.3: стрибок продуктивності, покращені generics і виявлення невикористаних змінних

PHPStan 2.3.0 працює до 7,5× швидше, ніж десять місяців тому, має розумніший кеш результатів, двонапрямне звуження типів для generics (new Collection(...)) і нові перевірки невикористаних змінних, присвоєнь та параметрів.

10
Pgvector Laravel
Новини 06 жовтня 2026

Простий векторний пошук подібності для Laravel: драйвер Pgvector для Scout

Бен Бьюрстром створив драйвер Pgvector для Laravel Scout, який автоматично підтримує актуальність векторних ембедингів і дає змогу шукати дані за змістом, а не лише за ключовими словами.

11

Вакансії за темою

Wooden Sword
8 днів тому

Senior PHP Developer (Game Backend)

Розробник backend для мобільної RPG з високим навантаженням. Основний стек: PHP Laravel API та TypeScript Nakama Game Server. Відповідальність за повний цикл розробки функцій — від логіки до тестування, code review, оптимізації та документації. Вимоги: 5+ років backend-досвіду, бездоганне знання Laravel/PHP, сильні навички TypeScript, PostgreSQL та асинхронного програмування.

Key2Law
21 день тому

Full Stack Developer (Laravel)

Full Stack розробник на Laravel для розробки та підтримки веб-додатків. Відповідальний за реалізацію backend (REST APIs, бізнес-логіка, оптимізація БД) і frontend (Blade, Tailwind, Alpine/Livewire/Vue), тестування, деплой та моніторинг. Вимоги: PHP 8.1+, Laravel 10+, знання Eloquent, Queue, Redis, MySQL, JavaScript ES6+, PHPUnit/Pest, Docker, Git, основи AWS. Бажано досвід з Fintech, криптовалютами, high-availability системами.

Z Global E-Commerce
7 днів тому

Full-Stack Developer (PHP/Laravel + Vue 3)

Full-Stack розробник з глибоким досвідом бекенду. Розробляєте масштабовані веб-платформи з комплексною бізнес-логікою, працюєте з PHP/Laravel, SQL, Vue 3/TypeScript. Основні вимоги: досвід з Laravel, Eloquent, проектування API, оптимізація баз даних (включно з мільйонами записів), фронтенд на Vue 3 з Composition API, TypeScript. Цінується розуміння архітектури, продуктивності й довгострокової підтримуваності коду.

Пакети за темою

Bagisto

bagisto/bagisto

Bagisto — це платформа для електронної комерції, побудована на Laravel. Вона надає готове рішення для створення та управління інтернет-магазинами з підтримкою каталогу товарів, замовлень, платежів та клієнтів.

28,211 v2.5.0-beta6 13 26

Lang

laravel-lang/lang

Список 126 мов для Laravel Framework, Laravel Jetstream, Laravel Fortify, Laravel Breeze, Laravel Cashier, Laravel Nova, Laravel Spark та Laravel UI.

7,778 15.37.3 12