Коли два воркери черги одночасно обробляють одне відправлення, вони обидва позначають його як «відправлене», і клієнт отримує посилку двічі. Стандартний Cache::lock() вирішує цю проблему, але з ним доводиться власноруч прописувати формат ключів, токени власників та звільнення блокування у блоці finally. Пакет Laravel Lock від Md Mahedi Zaman Zaber спрощує цей процес за допомогою builder, який приймає назву дії та об'єкт, зберігаючи блокування в кеші або базі даних.
# Основні можливості
- Fluent builder:
Lock::for('shipment_dispatch', $shipment)->ttl(120)->acquire()повертає boolean, а методblock()обгортає callback-функцію, автоматично звільняючи блокування. - Блокування на рівні моделей: Трейт
HasLocksдодає метод$shipment->lock('dispatch'). Ключ автоматично містить morph class моделі та її primary key. - Route middleware: Псевдонім
lockактивує блокування перед виконанням контролера та звільняє його після завершення, навіть якщо виникла помилка. - Два драйвери зберігання:
cache(для будь-якого сховища кешу Laravel) таdatabase(для таблиціlocks, дані в якій зберігаються навіть після очищення кешу). - Очікування: Методи
acquire()таblock()дозволяють вказати час у секундах для повторних спроб перед скасуванням. - Інспектування: Об'єкт
LockInfo(тільки для читання) надає доступ до ключа, токена власника, часу експірації та допоміжних методів на кшталтremainingSeconds()чиisOwnedBy().
# Отримання та звільнення блокувань
Фасад Lock створює очікуване блокування на основі назви дії та (опціонально) об'єкта. Зберігайте цей об'єкт у змінній, оскільки звільнення має виконуватися тим самим екземпляром:
use ZaberDev\Lock\Facades\Lock;
$lock = Lock::for('shipment_dispatch', $shipment)->ttl(120);
if ($lock->acquire()) {
try {
$carrier->dispatch($shipment);
} finally {
$lock->release();
}
}
Кожен екземпляр builder генерує унікальний UUID токен. Драйвери перевіряють цей токен перед видаленням запису. Якщо ви створите новий екземпляр Lock::for(...) і спробуєте викликати release() на ньому, він матиме інший токен і нічого не видалить. Для синхронізації між різними процесами токен можна встановити вручну через 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. В чергах це призводить до помилки виконання job замість мовчазного пропуску роботи.
Обидва методи підтримують очікування. acquire() приймає кількість секунд для спроб, а block() — третій аргумент із затримкою 250 мс між спробами:
$lock->acquire(blockSeconds: 5);
Lock::for('stock_allocation', $warehouse)->block($callback, 60, 5);
Для перевірки стану без захоплення існують методи isLocked(), isOwnedByCurrent(), remaining() та info(). Метод 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 class (із заміною зворотних слешів на підкреслення) та primary key: dispatch:App_Models_Shipment:42. Пакет також підтримує скалярні значення та інтерфейс Lockable для Value Objects.
При використанні бази даних HasLocks додає зв'язок locks() для перегляду активних блокувань моделі:
$shipment->locks()->where('expires_at', '>', now())->get();
# Захист маршрутів
Middleware lock дозволяє захищати endpoint. Ви можете вказати дію, TTL та драйвер:
Route::post('/warehouse/reconcile', [ReconcileController::class, 'store'])
->middleware('lock:warehouse_reconcile,300');
Щоб обмежити блокування конкретним записом, використовуйте параметри маршруту. Middleware автоматично замінить {shipment} на значення параметра або primary key моделі:
Route::post('/shipments/{shipment}/dispatch', [ShipmentController::class, 'dispatch'])
->middleware('lock:shipment_dispatch:{shipment},60');
Якщо блокування вже існує, middleware викидає LockAcquisitionException з кодом 423 (Locked). Оскільки це не HTTP-виняток, за замовчуванням клієнт отримає 500 помилку. Рекомендується обробити цей виняток вручну:
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);
});
# Сховища: кеш або база даних
Драйвер за замовчуванням налаштовується в config/locks.php через змінну LOCK_DRIVER. Метод using() дозволяє змінити драйвер для конкретного випадку:
Lock::for('inventory_sync', $warehouse)->using('cache')->ttl(15)->acquire();
Драйвер cache швидкий і використовує атомарність Cache::add(). Драйвер database надійніший, оскільки використовує транзакції та lockForUpdate(), а дані зберігаються навіть після перезавантаження Redis.
Застарілі записи в базі видаляються під час читання або через механізм Prunable:
use ZaberDev\Lock\Models\LockModel;
Schedule::command('model:prune', ['--model' => LockModel::class])->daily();
Пакет підтримує події: LockAcquired, LockFailed та LockReleased. Моніторинг LockFailed допомагає виявляти та виправляти race conditions у вашому застосунку.
# Встановлення
Laravel Lock потребує PHP 8.2+ та підтримує Laravel 11, 12 та 13:
composer require zaber-dev/laravel-lock
Публікація конфігурації та міграцій:
php artisan vendor:publish --tag=locks-config
php artisan vendor:publish --tag=locks-migrations
php artisan migrate
Сирці та документація доступні в репозиторії Laravel Lock на GitHub.