---
title: "Laravel Lock: пакет для розподілених блокувань моделей та маршрутів"
url: https://laravelukraine.com/blog/laravel-lock-paket-dlia-rozpodilenix-blokuvan-modelei-ta-marsrutiv
date: 2026-08-18
source: https://laravel-news.com/laravel-lock?utm_medium=feed&utm_source=feedpress.me&utm_campaign=Feed%3A+laravelnews
---

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

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

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

- **Зручний білдер**: `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 з рядка дії та опціональної цілі. Зберігайте цей білдер у змінній, бо звільнення має відбуватися з того самого екземпляра:

```php
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:

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

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

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

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

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

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

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

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

class Shipment extends Model
{
    use HasLocks;
}
```

```php
$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`, для отримання списку блокувань, які модель утримує:

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

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

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

```php
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 не згадує цю форму, але тести пакета її використовують:

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

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

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

```php
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()` перемикає його для одного блокування:

```php
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](https://laravel-news.com/atomic-cache-locks). Це швидший з двох варіантів і підходить для коротких блокувань на Redis або Memcached.

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

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

```php
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` показує, які дії у вашому додатку насправді конкурують, що корисно при налагодженні [умов гонитви](https://laravel-news.com/detecting-and-fixing-race-conditions-in-laravel-applications).

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

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

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

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

```bash
composer require zaber-dev/laravel-lock
```

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

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

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

Вихідний код та документація доступні у [GitHub репозиторії Laravel Lock](https://github.com/zaber-dev/laravel-lock).
