Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Події
Інше
Про нас
Переклади 18 серпня 2026

Laravel Lock: пакет для розподілених блокувань моделей та маршрутів

Коли два обробники черги одночасно беруть у роботу одне й те саме відправлення, обидва позначають його як відправлене, і клієнт отримує посилку двічі. Cache::lock() вже вирішує цю проблему, але кожного разу доводиться самостійно писати формат ключа, токен власника та пам'ятати про звільнення блокування у блоці finally. Laravel Lock від Md Mahedi Zaman Zaber ховає це за зручним білдером, який приймає назву дії та ціль, і зберігає блокування або в кеші, або в таблиці бази даних.

Основні можливості

  • Зручний білдер: Lock::for('shipment_dispatch', $shipment)->ttl(120)->acquire() повертає boolean, а block() обгортає callback, тому вам не потрібно писати код для звільнення блокування.
  • Блокування на рівні моделей: трейт HasLocks додає метод $shipment->lock('dispatch'), і ключ включає morph-клас моделі та первинний ключ.
  • Middleware для маршрутів: аліас lock, який встановлює блокування перед виконанням контролера і звільняє його після, навіть якщо контролер викидає виняток.
  • Два драйвери зберігання: cache для будь-якого Laravel cache store, database для таблиці locks, яка переживає очищення кешу.
  • Очікування: і acquire(), і block() приймають кількість секунд для повторних спроб перед тим, як здатися.
  • Інспекція блокувань: readonly об'єкт LockInfo з ключем, токеном власника та часом закінчення, плюс допоміжні методи на кшталт remainingSeconds() та isOwnedBy().

Отримання та звільнення блокувань

Lock фасад створює pending lock з рядка дії та опціональної цілі. Зберігайте цей білдер у змінній, бо звільнення має відбуватися з того самого екземпляра:

use ZaberDev\Lock\Facades\Lock;
$lock = Lock::for('shipment_dispatch', $shipment)->ttl(120);
if ($lock->acquire()) {
    try {
        $carrier->dispatch($shipment);
    } finally {
        $lock->release();
    }
}

Кожен білдер генерує власний UUID токен власника при першій потребі, і обидва драйвери перевіряють цей токен перед видаленням. Якщо створити другий Lock::for(...) і викликати на ньому release(), він матиме інший токен, тому звільнення нічого не зробить і поверне false. Ви можете встановити токен самостійно через owner('worker-7'), коли отримання та звільнення відбуваються у різних процесах.

Типовий TTL - 60 секунд. Поряд з ttl() є методи forSeconds() та forMinutes(), а refresh() продовжує блокування, яке ви все ще утримуєте, без попереднього звільнення.

block() виконує ту саму роботу одним викликом і повертає те, що повертає callback:

$manifest = Lock::for('shipment_dispatch', $shipment)->block(function () use ($shipment, $carrier) {
    return $carrier->dispatch($shipment);
});

Якщо блокування вже зайняте, block() викидає LockAcquisitionException замість повернення null, і виняток містить LockInfo для блокування, яке його затримує. У черговому завданні це означає, що завдання провалиться замість тихого пропуску роботи.

Обидва методи можуть очікувати замість негайної невдачі. acquire() приймає кількість секунд для повторних спроб, а block() приймає це як третій аргумент, з паузою 250 мілісекунд між спробами:

$lock->acquire(blockSeconds: 5);
Lock::for('stock_allocation', $warehouse)->block($callback, 60, 5);

Для перевірки без отримання є методи isLocked(), isOwnedByCurrent(), remaining() та info(), а enforce() викидає виняток, якщо хтось інший утримує блокування. forceRelease() видаляє запис незалежно від власника, для очищення після воркера, який загинув, утримуючи блокування.

Блокування моделі

Додайте трейт HasLocks до моделі, і ціль заповниться автоматично:

use Illuminate\Database\Eloquent\Model;
use ZaberDev\Lock\HasLocks;

class Shipment extends Model
{
    use HasLocks;
}
$lock = $shipment->lock('dispatch')->ttl(120);
$shipment->isLocked('dispatch');
$shipment->forceReleaseLock('dispatch');

Ключ складається з дії, потім morph-класу з backslash'ами, заміненими на підкреслення, потім первинного ключа: dispatch:App_Models_Shipment:42. Зареєструйте morph map, і ви отримаєте коротший аліас. Скалярні цілі також працюють, тому Lock::for('stock_allocation', $sku) дає вам stock_allocation:SKU-1180.

Для цілі, яка не є Eloquent моделлю, існує інтерфейс Lockable з одним методом getLockTargetIdentifier(): string. Реалізуйте його на value object, і цей рядок стане другою половиною ключа.

На database драйвері HasLocks також надає morph-відношення locks(), що вказує на таблицю locks, для отримання списку блокувань, які модель утримує:

$shipment->locks()->where('expires_at', '>', now())->get();

Захист маршруту

Сервіс-провайдер реєструє аліас middleware lock. Передайте йому дію, TTL у секундах та драйвер, якщо не хочете використовувати типовий:

Route::post('/warehouse/reconcile', [ReconcileController::class, 'store'])
    ->middleware('lock:warehouse_reconcile,300');
Route::post('/warehouse/reindex', [ReindexController::class, 'store'])
    ->middleware('lock:warehouse_reindex,600,database');

Ця форма не передає ціль, тому блокування покриває endpoint для всіх. Одна операція reconcile виконується одночасно, незалежно від того, хто її запустив.

Щоб обмежити блокування одним записом, помістіть параметр маршруту в назву дії. Middleware замінює {shipment} на значення цього параметра або на його primary key, коли параметр є прив'язаною моделлю. README не згадує цю форму, але тести пакета її використовують:

Route::post('/shipments/{shipment}/dispatch', [ShipmentController::class, 'dispatch'])
    ->middleware('lock:shipment_dispatch:{shipment},60');

Другий запит для того самого відправлення відхиляється, поки перший ще виконується. Запити для різних відправлень взагалі не бачать один одного. Middleware не чекає, тому тут немає черги, лише відхилення.

Коли блокування вже зайняте, middleware викидає LockAcquisitionException перед виконанням контролера. Він розширює RuntimeException, а не один з HTTP-винятків Laravel, тому необроблений виняток доходить до клієнта як 500. Код винятку - 423, що відповідає статусу 423 Locked, але нічого автоматично не застосовує це до відповіді. Обробіть виняток, щоб вибрати статус для клієнта:

use ZaberDev\Lock\Exceptions\LockAcquisitionException;

$exceptions->render(function (LockAcquisitionException $e) {
    return response()->json([
        'message' => 'Already processing. Try again in a moment.',
        'retry_after' => $e->lockInfo?->remainingSeconds(),
    ], 429);
});

Звільнення відбувається у блоці finally навколо $next($request). Помилка валідації або необроблений виняток звільняє блокування так само, як і відповідь 200.

Зберігання у кеші або базі даних

config/locks.php встановлює типовий драйвер через змінну середовища LOCK_DRIVER, а using() перемикає його для одного блокування:

Lock::for('inventory_sync', $warehouse)->using('cache')->ttl(15)->acquire();
Lock::for('stock_reconciliation', $warehouse)->using('database')->ttl(600)->acquire();

Cache драйвер зберігає невеликий payload під префіксом lock: і використовує Cache::add() для атомарності - той самий примітив, що стоїть за атомарними блокуваннями кешу Laravel. Це швидший з двох варіантів і підходить для коротких блокувань на Redis або Memcached.

Database драйвер записує рядок у таблицю locks з унікальною колонкою key, і кожне отримання виконується у транзакції, яка робить select з lockForUpdate() перед вставкою. Ці рядки залишаються після очищення кешу або перезапуску Redis, і ви можете запитувати їх через Eloquent. Ви платите записом і блокуванням рядка за кожне отримання.

Прострочені рядки видаляються двома способами. Читання прострочного блокування видаляє його, а LockModel використовує трейт Prunable Laravel для решти:

use Illuminate\Support\Facades\Schedule;
use ZaberDev\Lock\Models\LockModel;

Schedule::command('model:prune', ['--model' => LockModel::class])->daily();

Три події спрацьовують, коли locks.events.dispatch увімкнено: LockAcquired з ключем, власником, TTL, expiry та LockInfo, LockFailed з ключем, власником і блокуванням, яке завадило, та LockReleased з прапорцем $forced, який відрізняє звичайне звільнення від forceRelease(). Прослуховування LockFailed показує, які дії у вашому додатку насправді конкурують, що корисно при налагодженні умов гонитви.

LockManager розширює клас Manager Laravel, тому ви реєструєте власний драйвер через Lock::extend():

Lock::extend('dynamodb', fn ($app) => new DynamoLockDriver($app['dynamodb']));

Встановлення

Laravel Lock потребує PHP 8.2 або новіше і підтримує Laravel 11, 12 та 13:

composer require zaber-dev/laravel-lock

Конфігурація та міграція публікуються окремими тегами, і міграція потрібна лише для database драйвера:

php artisan vendor:publish --tag=locks-config
php artisan vendor:publish --tag=locks-migrations
php artisan migrate

Пакет також постачається з Laravel Boost skill, що охоплює його API-патерни. Він публікується під тегом locks-skill, і сервіс-провайдер копіює його до .ai/skills/laravel-lock під час виконання Artisan команди, якщо знайде встановлений Boost або існуючу директорію .ai/skills.

Вихідний код та документація доступні у GitHub репозиторії Laravel Lock.

Переклад оригіналу: laravel-news.com
17

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

Коментарі

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

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

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

NativePHP v4
Переклади 18 серпня 2026

NativePHP v4: створення нативного UI для iOS та Android у Blade

NativePHP v4 представляє SuperNative - технологію рендерингу Blade-компонентів як справжніх SwiftUI та Jetpack Compose елементів без web view. Нова версія підтримує Livewire-подібні компоненти, тестування у Pest та пряме розділення пам'яті між PHP та нативним шаром.

5

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

TomPlay
16 днів тому

Back-End Developer (PHP / Laravel)

Back-end розробник на початку кар'єри для роботи з PHP-системами Tomplay: веб-сайт на Laravel 10 та API на чистому PHP з високим трафіком. Основні обов'язки: розробка фічей, оптимізація MySQL-запитів, налагодження production-проблем, робота з тестуванням. Вимоги: 1-3 роки досвіду з PHP та Laravel, сильні SQL-навички, Git, REST API, англійська на рівні good, самоорганізація для remote-роботи.

Alliance Digital Нова
Сьогодні

PHP Developer

Middle PHP-розробник для фінтех-продукту. Розробка backend на Laravel з PostgreSQL, RabbitMQ, Redis. Реалізація складної бізнес-логіки кредитування, фінансових операцій, зовнішніх інтеграцій. Вимоги: 3+ років PHP, впевнена робота з Laravel, OOP/SOLID, складна бізнес-логіка, PostgreSQL, черги, тестування, production-mindset.

SendPulse Нова
Сьогодні

Middle+ PHP Developer (with DevOps)

Middle+ PHP Developer з DevOps-експертизою для мультикоманди, що розробляє CRM та освітню платформу. Основна роль: проєктування й реалізація модулів на PHP 8 і Laravel, оптимізація під високе навантаження, розробка інтеграцій. DevOps-компонент: налаштування CI/CD, контейнеризація (Docker/Kubernetes), управління AWS-інфраструктурою. Вимоги: глибока експертиза Laravel, Docker, Kubernetes, Git, технічна англійська.

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

Bagisto

bagisto/bagisto

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

27,987 v2.4.10 12 21

Lang

laravel-lang/lang

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

7,776 15.34.3 10